目录

(以下排序方法都按从小到大升序排列为例)
直接插入排序比较简单,但是它用途很广泛,其基本实现思路为:
从给定数组的1下标开始,在该下标的左边找比它小的数据,找到后就把小的数据往前移,找完一个下标的,再找另一个下标的。

举个栗子:
给定一个序列array{1,5,3,4,7,2,3},按照直接插入排序的思路把它变为一个升序序列。

如果 j 下标的值大于 i 下标的值,那么就让 j 下标的值往后退一下,即array[j+1] = array[ j ]

当 j 再往前走,发现 j 下标的值小于 i 下标的值,那么 j 停止移动,说明 j 之前已经是排好序的了,此时我们把,存放 i 的值的tmp赋给 j + 1下标,就完成了小的数字前移,大的数字后移:
经过不停地循环,直到……

此时, i 走到数组最后时又找到了一个 3 ,继续刚才的循环,看看这个 3 究竟会被放在哪里:

我们发现,3 被放在了 3 的后面,在排序前,3 也位于 3 的后面,而此时数组的排序已经完成了,那么我们就称这个直接插入排序是一个稳定的排序算法。
稳定性:如果在一个待排序的序列中,有多个相同的元素,在经过排序后,这些元素在序列中的位置相对不变,则称这个排序算法是稳定的;否则为不稳定的。

- public static void insertSort(int[] array){
- for (int i = 1; i < array.length; i++) {
- int j = i - 1;
- int tmp = array[i];
- for (; j >= 0 ; j--) {
- if (array[j] > tmp){
- array[j+1] = array[j];
- }else {
- break;
- }
- }
- array[j+1] = tmp;
- }
- }

根据直接插入排序的时间复杂度,我们可以得出一个结论:
数据越有序,直接插入排序的速度就越快。
希尔排序应运而生。
希尔排序的基本思想:对一个数组进行分组,然后对分组后的每组数据进行直接插入排序,不断地重复这个分组、排序的工作,当最后分为一组排序后,这组数据就成为了有序数组。
Q1:为什么要分组排序呢?
A1:因为分组可以加快排序的速度。
假设我们现在有10000个待排序的数据:

Q2:如何分组?

Q3:两种分组有什么区别呢?
我们对两种分组中的每组数据都进行直接插入排序,得到以下结果:

可以发现,通过希尔排序分组的数据,在排序之后,较大的数字都会放在后边,较小的数字都会放在前边,数组逐渐趋于有序。
而一般情况分组的数据在经过排序后,不一定会达到上述效果。
所以我们选择第二种分组方式。
希尔排序又叫做缩小增量排序,这个增量指的就是每次所分的组的数量,当这个增量不断减小,数组就会不断趋于有序,当增量为1时,再进行一次排序,数组就会变得有序。
那么问题来了~
Q4:如何确定增量?
A4:
希尔排序的分析是一个复杂的问题,因为它的时间是所取“增量”序列的函数,这涉及一些数学上尚未解决的难题。因此,到目前为止尚未有人求得一种最好的增量序列。
——严蔚敏《数据结构(C语言版)》
目前没有一个准确的确定增量的方法,你可以选择gap = n / 2,或者gap = n / 3,或者是其他的,只要让增量逐渐减小到1就行(gap表示增量,n表示数组长度)。
希尔排序本身就是对直接插入排序的一种优化排序,我们只要把刚才的直接插入排序稍微改动一下,即可实现希尔排序:

如果分组后的一组数据内不止2个元素,有多个元素呢?也是同理:

先比较一次红色组的数据,再比较一次绿色组的数据,只要我们每次让 j -= gap ,那么每次比较时,每组数据就不会互相影响:

- private static void shell(int[] array, int gap){
- for (int i = gap; i < array.length; i++) {
- int j = i - gap;
- int tmp = array[i]; //把i的值先存起来
- for (; j >= 0; j -= gap) {
- if (array[j] > tmp) {
- array[j + gap] = array[j];//此时需要交换的是j和j+gap下标的值
- } else {
- break;
- }
- }
- array[j + gap] = tmp;//把tmp的值赋给j+gap下标
- }
- }
- public static void shellSort(int[] array){
- int gap = array.length / 2; //自己确定gap的取值
- while (gap > 1){
- shell(array,gap); //每次分组后都进行一次直接插入排序
- gap /= 2;
- }
- shell(array,gap);//最后一次排序
- }
由于增量gap没有一个标准的取值方法,所以希尔排序的时间复杂度是不固定的。

选择排序又叫直接选择排序,它的实现思路非常简单:每次从待排序的数据中选择最小值(或最大值)放到序列的起始位置,直到全部待排序的数据排完。

- public class SelectSort {
- public static void swap(int[] array,int i,int j){
- int temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
- public static void selectSort(int[] array){
- for (int i = 0; i < array.length; i++) {
- int minIndex = i;
- for (int j = i + 1; j < array.length ; j++) {
- if(array[j] < array[minIndex]){
- minIndex = j;
- }
- }
- swap(array,i,minIndex);
- }
- }
- }
上述代码中,每次遍历待排序的序列只找到了最小值的下标,如果每次遍历既可以找到最小值的下标,又可以找到最大值的下标,那么排序速度就会变快很多:
- public class SelectSort {
- public static void swap(int[] array,int i,int j){
- int temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
-
- public static void selectSort2(int[] array){
- int left = 0;
- int right = array.length - 1;
- while (left < right){
- int minIndex = left;//记录最小值的下标
- int maxIndex = left;//记录最大值的下标
- for (int i = left; i < right; i++) {
- if(array[i] < array[minIndex]){
- minIndex = i;
- }
- if(array[i] > array[maxIndex]){
- maxIndex = i;
- }
- }
- //最小值移动到序列前段
- swap(array,left,minIndex);
- //判断left处是否为最大值的下标
- if(left == maxIndex){
- maxIndex = minIndex;
- }
- //最大值移动到序列末端
- swap(array,right,maxIndex);
- left++;
- right--;
- }
- }
- }
当 maxIndex 和 left 相同时,需要将 maxIndex 指向 minIndex (因为前一步的 swap()方法把 left 和 minIndex 的值进行了交换):

| 时间复杂度 | O(N^2) |
| 空间复杂度 | O(1) |
| 稳定性 | 不稳定 |
(1)创建一个大根堆(降序创建小根堆);
(2)将堆顶元素(最大值)和待排序元素中的最后一个元素进行交换;
(3)交换完成后向下调整。然后重复第二步,直到排序完所有元素。
- public class HeapSort {
- public static void swap(int[] array,int i,int j){
- int temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
- public void heapSort(int[] array){
- createBigHeap(array);
- int end = array.length - 1;
- while (end > 0){
- //交换堆顶元素和最后一个元素
- swap(array,0,end);
- //向下调整堆顶元素
- shiftDown(array,0,end);
- end--;
- }
- }
- //创建大根堆
- public void createBigHeap(int[] array){
- for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
- shiftDown(array,parent,array.length);
- }
- }
- public void shiftDown(int[] array,int parent,int len){
- int child = parent * 2 + 1;//左孩子节点的下标
- while (child < len){
- //判断是否有右孩子,并且右孩子的值是否大于左孩子的值
- if(child + 1 < len && array[child + 1] > array[child]){
- child += 1;
- }
- //判断较大的孩子节点的值和双亲节点的值的大小
- if(array[child] > array[parent]){
- swap(array,child,parent);
- //交换完成后需要判断后续的子堆是否为大根堆
- parent = child;
- child = parent * 2 + 1;
- }else {
- break;
- }
- }
- }
- }
| 时间复杂度 | O(N*logN) |
| 空间复杂度 | O(1) |
| 稳定性 | 不稳定 |
冒泡排序的思路很容易理解:每次从0下标开始,把当前元素和下一个元素进行比较,如果当前元素大于它下一个元素,则进行交换,直到把最大的元素换到待排序序列的最后一个位置。

- public class BubbleSort {
- public static void swap(int[] array,int i,int j){
- int temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
- public void bubbleSort(int[] array){
- int len = array.length;
- for (int i = 0; i < len - 1; i++) {
- //i确定排序的趟数,一共有len个元素,最多排len - 1趟
-
- boolean flg = false;
-
- for (int j = 0; j < len - 1 - i; j++) {
- //j确定每趟排序需要比较的元素个数
- //每排一趟就减少一个比较的元素
- if(array[j] > array[j+1]){
- swap(array,j,j+1);
- flg = true;
- }
- }
- //如果flg为false,说明这趟排序中一对元素都没有进行交换
- //也说明当前序列已经是有序序列了,没必要再进行后续的比较了
- if(!flg){
- break;
- }
- }
- }
- }
| 时间复杂度 | O(N^2) |
| 空间复杂度 | O(1) |
| 稳定性 | 稳定 |
(1)任取待排序元素中的某一个元素作为基准值;
(2)将当前序列分割成两个子序列,左子序列的元素均小于基准值,右子序列的元素均大于基准值;
(3)左右子序列重复上述两个步骤,直到待排序序列变为有序。
将待排序序列按基准值划分为左右两个部分的常见方式有以下三种:
主要思路:
(1)找基准值下标:从数组末端(right)往前找比数组首元素小的值,再从数组前端(left)往后找比数组首元素大的值,然后交换两个数值,right继续往前找,left继续往后找,直到二者相遇,最后把相遇位置的值和起始位置元素的值进行交换,相遇位置就是基准值所在的下标。
(2)根据基准值,递归基准值左边的子序列,再递归基准值右边的子序列。

- public class QuickSort {
- public void swap(int[] array,int i, int j){
- int temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
-
- //找基准值下标
- public int getPivot(int[] array,int left, int right){
- //记录left的起始位置
- int temp = left;
- //记录基准值
- int key = array[left];
- while (left < right){
- //从右边找比基准值小的元素的下标
- while (left < right && array[right] >= key){
- right--;
- }
- //从左边找比基准值大的元素的下标
- while (left < right && array[left] <= key){
- left++;
- }
- //交换
- swap(array,left,right);
- }
- //left和right相遇点即基准值下标
- //交换起始位置元素和基准值
- swap(array,left,temp);
- return left;
- }
-
- public void hoare(int[] array,int start, int end){
- //递归终止条件
- if(start >= end){
- return;
- }
- //获取基准值下标
- int pivot = getPivot(array,start,end);
- //递归基准值左子序列
- hoare(array,start,pivot-1);
- //递归基准值右子序列
- hoare(array,pivot+1,end);
- }
-
- public void sort(int[] array){
- hoare(array,0, array.length - 1);
- }
- }
主要思路:
(1)找基准值下标:将当前序列的其实元素存起来,然后从序列末端(right)找比当前序列起始位置小的元素,找到后,把该元素移到序列前端(left)位置,再从序列前端往后找比起始位置大的元素,找到后,把该元素移到right位置。
(2)重复循环上述步骤,直到left和right相遇,相遇点就是基准值下标,把起始位置的元素放到基准值下标处。
(3)根据基准值,递归基准值左边的子序列,再递归基准值右边的子序列。
- public class QuickSort2 {
- public void swap(int[] array,int i, int j){
- int temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
-
- //找基准值下标
- public int getPivot(int[] array,int left, int right){
- //存放起始位置元素
- int key = array[left];
- while (left < right){
- //从后往前找比key小的元素
- while (left < right && array[right] >= key){
- right--;
- }
- //找到后放到left位置
- array[left] = array[right];
- //从前往后找比key大的元素
- while (left < right && array[left] <= key){
- left++;
- }
- //找到后放到right位置
- array[right] = array[left];
- }
- //left和right相遇位置即基准值下标
- //把起始位置元素放到基准值下标处
- array[left] = key;
- return left;
- }
-
- public void hole(int[] array, int start, int end){
- //递归终止条件
- if(start >= end){
- return;
- }
- //获取基准值下标
- int pivot = getPivot(array,start,end);
- //递归基准值左子序列
- hole(array,start,pivot-1);
- //递归基准值右子序列
- hole(array,pivot+1,end);
- }
-
- public void sort(int[] array){
- hole(array,0, array.length - 1);
- }
- }
主要思路:
(1)刚开始时,定义prev指针指向序列起始位置,定义cur指针指向prev的后面一个位置,cur和prev同时向后一步一步走,当cur遇到比起始位置元素大的值时,prev这次不往后走,cur往后走,当cur再次遇到比起始位置元素小的值时,交换prev和cur位置的元素。
(2)当cur遍历完整个序列后,prev所在位置即基准值下标,交换序列起始位置的元素和基准值下标的元素。
(3)根据基准值,递归基准值左边的子序列,再递归基准值右边的子序列。
- public class QuickSort3 {
- public void swap(int[] array,int i, int j){
- int temp = array[i];
- array[i] = array[j];
- array[j] = temp;
- }
- //找基准值下标
- public int getPivot(int[] array,int left, int right){
- //存放起始位置元素
- int key = array[left];
- int prev = left;
- int cur = left + 1;
- while (cur <= right){
- //cur向后找比key大的值
- if(array[cur] < key && array[++prev] != array[cur]){
- swap(array,prev,cur);
- }
- cur++;
- }
- //遍历完整个数组后,prev所在的位置就是基准值下标
- //交换起始位置元素和基准值下标的元素
- swap(array,prev,left);
- return prev;
- }
- public void doublePointer(int[] array, int start, int end){
- //递归终止条件
- if(start >= end){
- return;
- }
- //获取基准值下标
- int pivot = getPivot(array,start,end);
- //递归基准值左子序列
- doublePointer(array,start,pivot-1);
- //递归基准值右子序列
- doublePointer(array,pivot+1,end);
- }
- public void sort(int[] array){
- doublePointer(array,0, array.length - 1);
- }
- }
上述三种快速排序的方式,在针对一个有序序列进行排序时,时间复杂度会达到O(N^2),并且,如果序列内的元素过多,递归的深度就会非常深,可能会导致栈溢出,因此,我们需要对上述三种方式进行优化。

(1)三数取中法选基准值
上述三种快速排序的方式,在进行第一次递归时,都是使用了0下标的值来作为初始的基准值,所以在针对有序序列进行排序时,时间复杂度会达到O(N^2),所以,只要我们不使用0下标作为初始基准值即可减少时间复杂度。
三数取中法是从待排序序列的首元素、最后一个元素和中间位置的元素,三个元素中选择一个数值第二大的元素和首元素进行交换,这样就可以在针对有序序列进行排序时,出现时间复杂度为O(N^2)的情况。
-
- //找第二大的数的下标
- public int getMidNumIndex(int[] array,int start,int end){
- //中间元素的下标
- int mid = (start + end) / 2;
-
- int num1 = array[start];
- int num2 = array[mid];
- int num3 = array[end];
-
- if(num1 > num2){
- if(num1 > num3){
- if(num2 > num3){
- return mid;
- }else {
- return end;
- }
- }else {
- return start;
- }
- }else {
- //num1 <= num2
- if(num2 > num3){
- if(num1 > num3){
- return start;
- }else{
- return end;
- }
- }else {
- return mid;
- }
- }
- }
找到第二大的元素后,再把它和首元素进行交换即可:
- public void hole(int[] array, int start, int end){
- //递归终止条件
- if(start >= end){
- return;
- }
-
- //交换首元素和第二大的元素
- int midNumIndex = getMidNumIndex(array,start,end);
- swap(array,start,midNumIndex);
-
- //获取基准值下标
- int pivot = getPivot(array,start,end);
- //递归基准值左子序列
- hole(array,start,pivot-1);
- //递归基准值右子序列
- hole(array,pivot+1,end);
- }

(2)递归到元素数量少的子区间时,可以考虑使用插入排序
假设在最好的情况,每次排序都可以通过基准值均分左右子序列,如果将每个子序列看做是一个节点,那么递归的所有结果可以看作一棵完全二叉树:

元素数量越少,递归的次数越多,时间复杂度也就越高,但是此时的子序列已经是趋于有序了,因此我们可以在元素数量较少的时候,使用插入排序来对剩余的元素进行排序,这样就可以大大减少时间复杂度。
- public void insertSort(int[] array, int start, int end){
- for (int i = start + 1; i <= end; i++) {
- int temp = array[i];
- int j = i - 1;
- for (; j >= start; j--) {
- if(array[j] > temp){
- array[j+1] = array[j];
- }else {
- break;
- }
- }
- array[j + 1] = temp;
- }
- }
- public void hole(int[] array, int start, int end){
- //递归终止条件
- if(start >= end){
- return;
- }
- if(end - start + 1 < 8){
- //子序列的元素小于8个的时候使用直接插入排序
- insertSort(array,start,end);
- return;
- }
- //交换首元素和第二大的元素
- int midNumIndex = getMidNumIndex(array,start,end);
- swap(array,start,midNumIndex);
-
- //获取基准值下标
- int pivot = getPivot(array,start,end);
- //递归基准值左子序列
- hole(array,start,pivot-1);
- //递归基准值右子序列
- hole(array,pivot+1,end);
- }
为了彻底解决栈溢出的问题,我们可以使用非递归版的快速排序:
主要思路:
(1)借助一个栈或者队列,把基准值的左、右子序列的起始位置和结束位置分别入队;

(2)每次弹出两个元素作为新区间的起始位置和结束位置,再次找基准值,循环第一步操作;

(3)当栈内为空时结束循环。
- public class QuickSort {
- //找基准值下标
- public int getPivot(int[] array,int left, int right){
- //存放起始位置元素
- int key = array[left];
- while (left < right){
- //从后往前找比key小的元素
- while (left < right && array[right] >= key){
- right--;
- }
- //找到后放到left位置
- array[left] = array[right];
- //从前往后找比key大的元素
- while (left < right && array[left] <= key){
- left++;
- }
- //找到后放到right位置
- array[right] = array[left];
- }
- //left和right相遇位置即基准值下标
- //把起始位置元素放到基准值下标处
- array[left] = key;
- return left;
- }
-
- //非递归版快速排序
- public void quickSort(int[] array,int start,int end){
- Stack
stack = new Stack<>(); - int pivot = getPivot(array,start,end);
- int left = start;
- int right = end;
-
- if(left < pivot - 1){
- stack.push(left);
- stack.push(pivot - 1);
- }
- if(right > pivot + 1){
- stack.push(pivot + 1);
- stack.push(right);
- }
-
- while (!stack.empty()){
- right = stack.pop();
- left = stack.pop();
- pivot = getPivot(array,left,right);
- if(left < pivot - 1){
- stack.push(left);
- stack.push(pivot - 1);
- }
- if(right > pivot + 1){
- stack.push(pivot + 1);
- stack.push(right);
- }
- }
- }
- }
| 时间复杂度(最好) | 时间复杂度(最坏) | 空间复杂度(最好) | 空间复杂度(最坏) | 稳定性 |
| O(N*logN) | O(N^2) | O(logN) | O(N) | 不稳定 |
(1)根据序列长度,将序列均分为左子序列和右子序列;

(2)对第一步进行递归,直到子序列中只有一个元素时进行合并;

(3)合并时,归并排序的问题就会被转换为对两个有序数组进行排序的简单问题。

- public class MergeSort {
- //分解
- public void mergeSort(int[] array,int left,int right){
- //终止条件
- if(left >= right){
- return;
- }
-
- int mid = (left + right) / 2;
-
- //1.分解左边
- mergeSort(array,left,mid);
- //2.分解右边
- mergeSort(array,mid + 1,right);
- //3.合并
- merge(array,left,right,mid);
- }
-
- //合并
- public void merge(int[] array,int start,int end,int midIndex){
- //记录左子序列的起始位置
- int ret = start;
- //创建一个临时数组
- int[] temp = new int[end - start + 1];
- //临时数组的下标
- int k = 0;
- //右子序列的起始位置
- int start2 = midIndex + 1;
-
- //两个子序列中都有元素的情况:
- while (start <= midIndex && start2 <= end){
- if(array[start] <= array[start2]){
- temp[k++] = array[start++];
- }else {
- temp[k++] = array[start2++];
- }
- }
- //只有一个子序列有元素的情况:
- while (start <= midIndex){
- temp[k++] = array[start++];
- }
- while (start2 <= end){
- temp[k++] = array[start2++];
- }
-
- //把temp数组的内容拷贝到array数组中
- for (int i = 0; i < temp.length; i++) {
- array[ret + i] = temp[i];
- }
- }
- }
非递归版的归并排序省去了分解的过程,直接开始合并:

- public class MergeSort {
- public void mergeSort(int[] array){
- //gap表示每个子序列的元素个数
- int gap = 1;
-
- while (gap <= array.length){
- for (int i = 0; i < array.length; i += gap*2) {
- //左子序列起始位置
- int s1 = i;
- //左子序列结束位置
- int e1 = i + gap - 1;
- //右子序列结束位置
- int e2 = e1 + gap;
- //如果e2超过数组长度就越界了
- if(e2 < array.length){
- merge(array,s1,e2,e1);//合并
- }
- }
- gap *= 2;
- }
- }
- //合并
- public void merge(int[] array,int start,int end,int midIndex){
- //记录左子序列的起始位置
- int ret = start;
- //创建一个临时数组
- int[] temp = new int[end - start + 1];
- //临时数组的下标
- int k = 0;
- //右子序列的起始位置
- int start2 = midIndex + 1;
-
- //两个子序列中都有元素的情况:
- while (start <= midIndex && start2 <= end){
- if(array[start] <= array[start2]){
- temp[k++] = array[start++];
- }else {
- temp[k++] = array[start2++];
- }
- }
- //只有一个子序列有元素的情况:
- while (start <= midIndex){
- temp[k++] = array[start++];
- }
- while (start2 <= end){
- temp[k++] = array[start2++];
- }
-
- //把temp数组的内容拷贝到array数组中
- for (int i = 0; i < temp.length; i++) {
- array[ret + i] = temp[i];
- }
- }
- }
| 时间复杂度 | O(N*logN) |
| 空间复杂度 | O(N) |
| 稳定性 | 稳定 |