排序算法

排序算法是《数据结构与算法》中最基本的算法之一。

排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:

1

1.冒泡排序

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

1.1 排序原理

  1. 比较相邻的元素。如果前一个元素比后一个元素大,就交换这两个元素的位置。
  2. 对每一对相邻元素做同样的工作,从开始第一对元素到结尾的最后一对元素。最终最后位置的元素就是最大值。

1.2 算法演示

2

最快的时候:输入数据已经是 正序 , 这时候只需要循环确定一遍,时间复杂度为 O(n)

最慢的时候:输入数据是 反序 ,这时候需要嵌套式循环移动,时间复杂度为 O(n^2)

1.3 代码实现

升序排序

public static int[] ascendingSort(int[] source) {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(source, source.length);

for (int i = 0; i < source.length - 1; i++) {
for (int j = 0; j < source.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
return arr;
}

降序排序

public static int[] descendingSort(int[] source) {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(source, source.length);

for (int i = 0; i < source.length - 1; i++) {
for (int j = 0; j < source.length - 1 - i; j++) {
if (arr[j] < arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
return arr;
}

代码测试

public static void main(String[] args) {
int[] source = {4, 2, 3, 1};
//升序排序
int[] ascendingSort = ascendingSort(source);
//降序排序
int[] descendingSort = descendingSort(source);
System.out.println("============冒泡排序=============");
System.out.println("原有数组为:" + Arrays.toString(source));
System.out.println("升序数组为:" + Arrays.toString(ascendingSort));
System.out.println("降序数组为:" + Arrays.toString(descendingSort));
}

3

2.选择排序

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处就是不占用额外的内存空间。

2.1 排序原理

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

重复第二步,直到所有元素均排序完毕。

2.2 算法演示

4

无论什么数据进去都是 O(n²) 的时间复杂度

2.3 代码实现

升序排序

public static int[] ascendingSort(int[] source) {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(source, source.length);

for (int i = 0; i < source.length - 1; i++) {
int min = i;
for (int j = i + 1; j < source.length; j++) {
//比较值,使 min 的值是最小的值的索引
if (arr[min] > arr[j]) {
min = j;
}
}
//交换 i 和 min 对应值
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
return arr;
}

降序排序

public static int[] descendingSort(int[] source) {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(source, source.length);

for (int i = 0; i < source.length - 1; i++) {
int min = i;
for (int j = i + 1; j < source.length; j++) {
if (arr[min] < arr[j]) {
min = j;
}
}
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
return arr;
}

代码测试

public static void main(String[] args) {
int[] source = {4, 2, 3, 1};
//升序排序
int[] ascendingSort = ascendingSort(source);
//降序排序
int[] descendingSort = descendingSort(source);
System.out.println("============选择排序=============");
System.out.println("原有数组为:" + Arrays.toString(source));
System.out.println("升序数组为:" + Arrays.toString(ascendingSort));
System.out.println("降序数组为:" + Arrays.toString(descendingSort));
}

5

3.插入排序

插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1 排序原理

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

3.2 算法演示

6

插入排序使用了双层for循环,其中内层循环的循环体是真正完成排序的代码,所以,分析插入排序的时间复杂度,主要分析内层循环体的执行次数:

最好情况:正序排列,只进行最外层的n次循环+n次的判断,时间复杂度 O(n)

最坏情况:倒序排列,外层n次循环+ 1+2+…+n的判断,时间复杂度 O(n^2)

3.3 代码实现

升序排序

public static int[] ascendingSort(int[] source) {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(source, source.length);

for (int i = 0; i < source.length; i++) {
//当前元素为a[i],依次和前面的元素比较,找到一个小于等于a[i]的元素
for (int j = i; j > 0; j--) {
if (arr[j - 1] > arr[j]) {
int tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
} else {
break;
}
}
}
return arr;
}

降序排序

public static int[] descendingSort(int[] source) {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(source, source.length);

for (int i = 0; i < source.length; i++) {
//当前元素为a[i],依次和前面的元素比较,找到一个小于等于a[i]的元素
for (int j = i; j > 0; j--) {
if (arr[j - 1] < arr[j]) {
int tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
} else {
break;
}
}
}
return arr;
}

代码测试

public static void main(String[] args) {
int[] source = {4, 2, 3, 1};
//升序排序
int[] ascendingSort = ascendingSort(source);
//降序排序
int[] descendingSort = descendingSort(source);
System.out.println("============插入排序=============");
System.out.println("原有数组为:" + Arrays.toString(source));
System.out.println("升序数组为:" + Arrays.toString(ascendingSort));
System.out.println("降序数组为:" + Arrays.toString(descendingSort));
}

7

4.希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行依次直接插入排序。

4.1 排序原理

  1. 选定一个增长量step,按照增长量h作为数据分组的依据,对数据进行分组;
  2. 对分好组的每一组数据完成插入排序;
  3. 减小增长量,最小减为1,重复第二步操作。

4.2 算法演示

8

9

平均时间复杂度 : O(n log n)

最好时间复杂度 : O(nlog^2*n)

最坏时间复杂度 : O(nlog^2*n)

4.3 代码实现

升序排序

public static int[] ascendingSort(int[] source) {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(source, source.length);
//控制增长量 从 arr.length / 2 开始
for (int step = arr.length / 2; step >= 1; step = step / 2) {
// a[i]就是待插入的元素
for (int i = step; i < arr.length; i++) {
//a[j]就是待插入元素,每次和前面的a[j-step]进行比较
for (int j = i; j >= step; j -= step) {
if (arr[j - step] > arr[j]) {
int tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
} else {
break;
}
}
}
}
return arr;
}

降序排序

public static int[] descendingSort(int[] source) {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(source, source.length);
for (int step = arr.length / 2; step >= 1; step = step / 2) {
for (int i = step; i < arr.length; i++) {
for (int j = i; j >= step; j -= step) {
if (arr[j - step] < arr[j]) {
int tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
} else {
break;
}
}
}
}

return arr;
}

代码测试

public static void main(String[] args) {
int[] source = {9,1,2,5,7,4,8,6,3,5};
//升序排序
int[] ascendingSort = ascendingSort(source);
//降序排序
int[] descendingSort = descendingSort(source);

System.out.println("============希尔排序=============");
System.out.println("原有数组为:" + Arrays.toString(source));
System.out.println("升序数组为:" + Arrays.toString(ascendingSort));
System.out.println("降序数组为:" + Arrays.toString(descendingSort));
}

10

5.归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归;(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法)
  • 自下而上的迭代;

5.1 迭代

定义:定义方法时,在方法内部调用方法本身,称之为递归.

作用:它通常把一个大型复杂的问题,层层转换为一个与原问题相似的,规模较小的问题来求解。递归策略只需要少量的程序就可以描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

注意:在递归中,不能无限制的调用自己,必须要有边界条件,能够让递归结束,因为每一次递归调用都会在栈内存开辟新的空间,重新执行方法,如果递归的层级太深,很容易造成栈内存溢出。

递归Dome

1!: 1
2!: 2x1=2x1!
3!: 3x2x1=3x2!
4!: 4x3x2x1=4x3!

n!: nx(n-1)x(n-2)…x2x1=nx(n-1)!

public class Iterate {
// 阶乘算法
public static int factorial(int n) {
if (n == 1) {
return 1;
}
return n * factorial(n - 1);
}
public static void main(String[] args) {
int result1 = factorial(1);
int result2 = factorial(2);
int result3 = factorial(3);
int result4 = factorial(4);
int result5 = factorial(5);
System.out.println("1!=" + result1);
System.out.println("2!=" + result2);
System.out.println("3!=" + result3);
System.out.println("4!=" + result4);
System.out.println("5!=" + result5);
}
}

11

5.2 排序原理

  1. 尽可能的一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是 1 为止。

  2. 将相邻的两个子组进行合并成一个有序的大组;

  3. 不断的重复步骤2,直到最终只有一个组为止。

5.3 算法演示

12

13

归并算法时间复杂度

15

用树状图来描述归并,如果一个数组有8个元素,那么它将每次除以2找最小的子数组,共拆 log8 = 3,所以树共有3层,那么自顶向下第k层有 2^k 个子数组,每个数组的长度为 2^(3-k),归并最多需要 2^(3-k) 次比较。因此每层的比较次数为 2^k * 2^(3-k) = 2^3,那么3层总共为 3 * 2^3

假设元素的个数为n,那么使用归并排序拆分的次数为 log2(n) , 所以共 log2(n) 层,那么使用 log2(n) 替换上面 3*2^3 中的3这个层数,最终得出的归并排序的时间复杂度为:log2(n) * 2^(log2(n)) = log2(n)*n , 根据大O推导法则,忽略底数,最终归并排序的时间复杂度为O(nlogn) ;

归并排序的缺点:

需要申请额外的数组空间,导致空间复杂度提升,是典型的以空间换时间的操作。

希尔排序归并排序在处理大批量数据时差别不是很大。

5.4 代码实现

升序排序为例

//该方法用于分割数组,递归成有序的数组 归并
public static int[] sort(int[] source) {
//复制数组
int[] arr = Arrays.copyOf(source, source.length);
//递归限制条件
if (arr.length < 2) {
return arr;
}
//中间值
int mid = (int) Math.floor(arr.length / 2);
//分割数组
int[] left = Arrays.copyOfRange(arr, 0, mid);
int[] right = Arrays.copyOfRange(arr, mid, arr.length);
//递归分割+归并
return merge(sort(left), sort(right));
}
//归并数组
public static int[] merge(int[] left, int[] right) {
//创建数组,承接排序后数组
int[] result = new int[left.length + right.length];
int i = 0;
//进行数组排序
while (left.length > 0 && right.length > 0) {
//下面的 if 进行判断,可以降序或者升序
if (left[0] < right[0]) {
result[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
} else {
result[i++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
}
while (left.length > 0) {
result[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
}
while (right.length > 0) {
result[i++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
return result;
}

代码测试

public static void main(String[] args) {
int[] source = {9, 1, 2, 5, 7, 4, 8, 6, 3, 5};
//排序
int[] sorted = sort(source);
System.out.println("============归并排序=============");
System.out.println("原有数组为:" + Arrays.toString(source));
System.out.println("排序后数组为:" + Arrays.toString(sorted));
}

14

6.快速排序

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n^2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

6.1 排序原理

  1. 首先设定一个分界值,通过该分界值将数组分成左右两部分;
  2. 将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
  3. 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
  4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。

6.2 算法演示

16

17

快速排序 和 归并排序的区别:

快速排序是另外一种分治的排序算法,它将一个数组分成两个子数组,将两部分独立的排序。

快速排序和归并排序是互补的:

归并排序 将数组分成两个子数组分别排序,并将有序的子数组归并从而将整个数组排序;

快速排序 的方式则是当两个数组都有序时,整个数组自然就有序了。

在归并排序中,一个数组被等分为两半,归并调用发生在处理整个数组之前,在快速排序中,切分数组的位置取决于数组的内容,递归调用发生在处理整个数组之后。

快速排序时间复杂度分析:

最优情况:每一次切分选择的基准数字刚好将当前序列等分,共切分了 logn次,所以,最优情况下快速排序的时间复杂度为 O(nlogn);

18

最坏情况:每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得每次切分都会有一个子组,那么总共就得切分n次,所以,最坏情况下,快速排序的时间复杂度为O(n^2) ;

19

平均情况:每一次切分选择的基准数字不是最大值和最小值,也不是中值,这种情况,快速排序的时间复杂度为O(nlogn)

6.3 代码实现

以升序排序举例

// 分割数组,排序
public static void sort(int[] source, int left, int right) {
if (left >= right) {
return;
}
//获得基准值
int partition = partition(source, left, right);
//循环分割左半部分
sort(source, left, partition - 1);
//循环分割右半部分
sort(source, partition + 1, right);
}

public static int partition(int[] source, int left, int right) {
//基准值pivot
int pivot = left;
int index = pivot + 1;
//移动数组元素,小于基准的在前,大于基准的在后
for (int i = index; i <= right; i++) {
//if中条件可以控制 升序还是降序
if (source[pivot] > source[i]) {
int t = source[index];
source[index] = source[i];
source[i] = t;
index++;
}
}
//将基准元素交换到对应位置
int t = source[index - 1];
source[index - 1] = source[pivot];
source[pivot] = t;
//返回基准元素索引值
return index - 1;
}

代码测试

public static void main(String[] args) {
int[] source = {9, 1, 2, 5, 7, 4, 8, 6, 3, 5};
System.out.println("============快速排序=============");
System.out.println("原有数组为:" + Arrays.toString(source));
sort(source, 0, source.length - 1);
System.out.println("排序数组为:" + Arrays.toString(source));
}

18

7.堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

7.1 排序原理

堆构造原理

  1. 创建一个新数组,把原数组数据拷贝到新数组
  2. 再从新数组长度的一半处开始往0索引处扫描
  3. 然后对扫描到的每一个元素做下沉调整即可

堆排序原理

  1. 创建一个堆 H[0……n-1];
  2. 把堆首(最大值)和堆尾互换;
  3. 把堆的尺寸缩小 1,并调用 下沉函数,目的是把新的数组顶端数据调整到相应位置;
  4. 重复步骤 2,直到堆的尺寸为 1;

7.2 算法演示

21

22

23

24

7.3 代码实现

以升序排序举例

//交换
public static void exch(int[] source, int i, int j) {
int t = source[i];
source[i] = source[j];
source[j] = t;
}

//下沉函数
public static void sink(int[] heap, int i, int len) {
int m = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
//通过下面两个if函数的比较,决定时升序还是降序
if (left < len && heap[m] < heap[left]) {
m = left;
}
if (right < len && heap[m] < heap[right]) {
m = right;
}
if (m != i) {
exch(heap, i, m);
sink(heap, m, len);
}
}

//排序算法
public static int[] sort(int[] source) {
int[] heap = Arrays.copyOf(source, source.length);
int len = heap.length;
//创建堆
for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
sink(heap, i, len);
}
//最顶与最后一个元素进行交换,之后下沉新数组
while (len > 1) {
exch(heap, 0, len - 1);
len--;
sink(heap, 0, len);
}
return heap;
}

代码测试

public static void main(String[] args) {
int[] source = {2, 4, 5, 1, 5, 1, 7};
System.out.println("============堆排序=============");
System.out.println("原有数组为:" + Arrays.toString(source));
source = sort(source);
System.out.println("升序数组为:" + Arrays.toString(source));
}

25

排序的稳定性

稳定性的定义:
数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,如果使用某种排序算法排序后,能够保证A元素依然在B元素的前面,可以说这个该算法是稳定的。

常见排序算法的稳定性:

冒泡排序:
只有当 arr[i]>arr[i+1] 的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定排序算法。

选择排序:
选择排序是给每个位置选择当前元素最小的,例如有数据 {5(1),8 ,5(2), 2, 9 } , 第一遍选择到的最小元素为2,所以5(1)会和2进行交换位置,此时5(1)到了5(2)后面,破坏了稳定性,所以选择排序是一种不稳定的排序算法。

插入排序:
比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

希尔排序:
希尔排序是按照不同步长对元素进行插入排序 ,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。

归并排序:
归并排序在归并的过程中,只有 arr[i]<arr[i+1] 的时候才会交换位置,如果两个元素相等则不会交换位置,所以它并不会破坏稳定性,归并排序是稳定的。

快速排序:
快速排序需要一个基准值,在基准值的右侧找一个比基准值小的元素,在基准值的左侧找一个比基准值大的元素,然后交换这两个元素,此时会破坏稳定性,所以快速排序是一种不稳定的算法。