一直想理解一下基本的排序算法,最近正好在搞java所以就一并了(为了便于理解,这儿都是以从小到大排序为目的)
冒泡排序
也就是比较连续的两个值,如果前面一个值大于后面一个值,则交换。
时间复杂度为O(n*n),是稳定排序(稳定性意思:当两个值相同时,排序前后的这两个值的相对位置是否有交换)
注意点:第二重循环参数
// 冒泡排序,从小到大 private static void BubbleSort(int[] arr, int n) { for (int i = 0; i < n; ++i) { for (int j = 0; j < n - i - 1; ++j) { if (arr[j] > arr[j + 1]) Swap(arr, j, j + 1); } } }
选择排序
就是从头到尾选择每个位置放这个位置到最后一个位置的最小值,其中我们可以标记,减少交换次数
时间复杂度为O(n*n),不稳定的排序
注意点:判断后再进行异或的交换
// 选择排序 private static void SelectSort(int[] arr, int n) { int temp; for (int i = 0; i < n; ++i) { temp = i; for (int j = i + 1; j < n; ++j) { if (arr[temp] > arr[j]) { temp = j; } } // 异或交换时必须判断 if (i != temp) Swap(arr, i, temp); } }
直接插入排序
就是从头到尾遍历每个点,每个点再从尾到头找这个值放在数组的位置
时间复杂度为O(n*n),稳定的排序
注意点:temp与谁比较
// 直接插入排序 private static void InsertSort(int[] arr, int n) { // 哨兵 int temp; for (int i = 1; i < n; ++i) { temp = arr[i]; int k = i - 1; // 注意从后向前 for (int j = k; j >= 0 && temp < arr[j]; --j, --k) { arr[j + 1] = arr[j]; } arr[k + 1] = temp; } }
快速排序
其实就类似分治的方法,取一个中枢元素,然后找到一个位置放这个中枢元素,保证这个位置前面的值不大于它,后面的值不小于它,接着分别递归
最坏时间复杂度为O(n*n),平均时间复杂度为O(n*log2n),所以可以先打乱顺序。是不稳定的排序
注意点:中间两个while的顺序
// 模拟洗牌打乱顺序 private static void Shuffle(int[] arr, int n) { Random random = new Random(); for (int i = 0; i < n; ++i) { int x = random.nextInt(n - 1); int y = random.nextInt(n - 1); if (x != y) Swap(arr, x, y); } Print(arr, n); } // 快速排序(最坏时间是n*n,所以我们首先可以打乱顺序) private static void quickSort(int[] arr, int left, int right) { // 哨兵 int temp = arr[left]; int i = left, j = right; // 出口 if (i >= j) return; // 保证左边一定不比temp大,右边一定不比temp小 while (i < j) { while (i < j && arr[j] >= temp) { --j; } if (i < j) { arr[i] = arr[j]; ++i; } while (i < j && arr[i] <= temp) { ++i; } if (i < j) { arr[j] = arr[i]; --j; } } arr[i] = temp; // 左右递归 quickSort(arr, left, i - 1); quickSort(arr, i + 1, right); }
二路归并排序
其实很简单,就是从中间分成左右两个数组分别递归,在最后返回时我们保证两个数组都是有序数组再合并,可以知道使用双指针的方法可以在O(n)的
时间内合并两个有序数组,然后知道这样分下去其实是几乎完全的二叉树,高度为log2n
时间复杂度为O(n*log2n),是稳定的排序
注意点:指针向后移动
// 二路归并排序 private static void Marge(int[] arr, int left, int mid, int right) { int[] temp = new int[right - left + 1]; int leftpos = left, rightpos = mid + 1; // 双指针方法 for (int i = 0; i < right - left + 1; ++i) { if (rightpos > right || leftpos <= mid && arr[leftpos] <= arr[rightpos]) { temp[i] = arr[leftpos++]; } else { temp[i] = arr[rightpos++]; } } for (int i = 0; i < right - left + 1; ++i) { arr[i + left] = temp[i]; } } private static void MargeSort(int[] arr, int left, int right) { if (left >= right) return; // 分成两段递归 int mid = (left + right >> 1); MargeSort(arr, left, mid); MargeSort(arr, mid + 1, right); // 关键:合并两端有序数组,只需用O(n)时间做到 Marge(arr, left, mid, right); }
希尔排序
极其简单的算法,简答的方法是是设置步长并每次减一半,接着使用插入排序,排的是相隔步长的元素
时间复杂度很麻烦,但是最坏也是O(n*n),平均大概是O(n^3/2),是不稳定排序
// 希尔排序 private static void HillSort(int[] arr, int n) { // 设置步长,即保证相隔步长个元素为有序数组 for (int step = n / 2; step > 0; step /= 2) // 修改的插入排序 for (int i = step; i < n; ++i) for (int j = i; j - step >= 0; j -= step) if (arr[j - step] > arr[j]) Swap(arr, j - step, j); }
堆排序
为了让额外的空间复杂度为O(1),可以首先倒着使用原数组下沉的思想(就是保证了此点的子树一定是堆)建立大顶堆,
接着依次将头放到新空出来的位置,再更新堆
时间复杂度是O(n*log2n),是不稳定排序
注意点:边界值得大小
// 堆的下沉,保证此树为堆 private static void DeleteEmp(int[] arr, int fat, int n) { int lefchild; int temp = arr[fat]; for (; (fat << 1 | 1) < n; fat = lefchild) { lefchild = fat << 1 | 1; if (lefchild < n - 1 && arr[lefchild] < arr[lefchild + 1]) lefchild++; if (arr[lefchild] > temp) { arr[fat] = arr[lefchild]; } else { break; } } arr[fat] = temp; } // 堆排序(额外的辅助空间为O(1)),聪明的方法是利用原来的空间直接建树, // 但是从小到大时我们需要建立大顶堆,接着删除第一个点放到数组最后一个位置,倒着插入 private static void HeapSort(int[] arr, int n) { // 建堆(注意普通建堆时间复杂度为O(n),因为上浮的平均时间复杂度为O(1)) // 保证以个点为树的都是堆,倒着来就可以保证整棵树 for (int i = n / 2; i >= 0; --i) { DeleteEmp(arr, i, n); } // 堆顶放入后面,再调整堆 for (int i = n - 1; i > 0; --i) { Swap(arr, 0, i); DeleteEmp(arr, 0, i); } }
以上都是基于比较的排序方法,接下来介绍一些非基于比较的排序算法,而且都是线性的时间复杂度
计数排序
就是将数字变成数组下标,这种排序主要是保证数据范围比较小。
时间复杂度为O(k),是不稳定的排序
基数排序
首先从低到高枚举的是数字的每一位,每一位根据0-9的顺序放入桶中储存,再放回原数组,注意负数,还有就是非常耗费空间
时间复杂度为O(n),是稳定的排序
// 基数排序,时间复杂度为O(n)的排序方式,m为最大数字的位数+1 private static void BaseSort(int[] arr, int n, int m) { // 假设数字范围为(0,1000000000) int[] order = new int[10]; int[][] temp = new int[10][n]; int mm = 0, k = 1; while (mm < m) { // 从最低位开始放入10个桶中 for (int i = 0; i < n; ++i) { int last = arr[i] / k % 10; temp[last][order[last]++] = arr[i]; } // 10个桶中按顺序放回数组 int coun = 0; for (int i = 0; i < 10; ++i) { if (order[i] > 0) { for (int j = 0; j < order[i]; ++j) arr[coun++] = temp[i][j]; order[i] = 0; } } k *= 10; ++mm; } }
桶排序
将数组分到有限数量的桶子里。每个桶子再个别排序
时间复杂度为O(n),是稳定的排序
接着还有一些比较奇特的排序方式:
鸡尾酒排序:双向冒泡排序
梳排序:在冒泡排序下让步长不为1,而且每次步长除以1.3
奇偶排序:多核处理有优势;先将待排序数组的所有奇数位与自己身后相邻的偶数位相比较,如果前者大于后者,则进行交换,直到这一趟结束。然后将偶数位与自己身后相邻的奇数位相比较,如果前者大于后者,则进行交换,直到这一趟结束。重复
外排序:以上都是在内存中排序的算法,即可以在内存中直接寻址,而外排序则可能是输入数据在磁带上,它使用的是归并排序,但是可以是多路归并排序
树形选择排序(锦标赛排序) :对N个数字,选出最小(大)的n/2个数,再进行新一轮的比较,直到选出最小(大)的。
1.把N个数放到完全二叉树的叶子节点,两两比较,选出最小的作为根节点,且保存到数组中
2.把最小的原始值设为无穷大,从那个地方开始新一轮比较
第一次需要比较n-1次,之后都是log2(n)次
小结
对于一般的内部排序,基本使用的是插入排序,希尔排序或者快速排序,主要是根据输入数据的大小来确定。
注意希尔排序可以使用Sedgewick增量运行,则预计运行时间就为O(N^7/6),而快速排序找枢纽可以使用三数中值分割法。
堆排序比希尔排序要慢,但是也可以根据Floyd提出的改进算法移动一次数据来优化。