目录
1. 概念
1.1 排序
排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。 平时的上下文中,如果提到排序,通常指的是排升序(非降序)。 通常意义上的排序,都是指的原地排序(in place sort)。
1.2 稳定性(重要)
两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。
2. 七大基于比较的排序-总览
3. 插入排序
3.1 直接插入排序-原理
整个区间被分为
- 有序区间
- 无序区间
每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入
3.2 实现
public static void insertSort(int[] array) {
int len = array.length;
for(int i = 1; i<len; i++) {
int tmp = array[i];
int j = i-1;
for(; j >= 0; j --) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
break;
}
}
array[j+1] = tmp;
}
}
3.3 性能分析
稳定性:稳定
插入排序,初始数据越接近有序,时间效率越高。
4. 希尔排序
4.1 原理
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时, 所有记录在统一组内排好序。
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
这样跳跃式的分组可能会将更小的元素尽可能往前放.
动图如下:
希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定
4. 稳定性:不稳定
4.2 实现
public static void shellSort(int[] array) {
int gap = array.length;
while (gap > 1) {
gap /= 2;
shell(array, gap);
}
}
private static void shell(int[] array, int gap) {
for(int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = i - gap;
for(; j >= 0; j -= gap) {
if(array[j] > tmp) {
array[j + gap] = array[j];
}else {
break;
}
}
array[j+gap] = tmp;
}
}
4.3 性能分析
5. 选择排序
5.1 直接选择排序-原理
每一次从无序区间选出最大(或最小)的一个元素,存放在无序区间的最后(或最前),直到全部待排序的数据元素 排完 。
5.2 实现
public static void selectSort(int[] array) {
int len = array.length;
for(int i = 0; i<len; i++) {
int minIndex = i;
for(int j = i+1; j<len; j++) {
if(array[j] < array[minIndex]) {
minIndex = j;
}
}
//交换
int tmp = array[i];
array[i] = array[minIndex];
array[minIndex] = tmp;
}
}
双向选择排序(了解)
每一次从无序区间选出最小 + 最大的元素,存放在无序区间的最前和最后,直到全部待排序的数据元素排完 。
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+1; i<=right; i++) {
if(array[i] < array[minIndex]) {
minIndex = i;
}
if(array[i] > array[maxIndex]) {
maxIndex = i;
}
}
swap(array, left, minIndex);
//注意可能情况: 最大值刚好在最小值的位置, 已经交换到了minIndex
if(left == maxIndex) {
maxIndex = minIndex;
}
swap(array, right, maxIndex);
left++;
right--;
}
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
5.4 性能分析
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
6. 堆排序
6.1 原理
基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的 数。
注意: 排升序要建大堆;排降序要建小堆。
6.2 实现
/**
* 时间复杂度: O(n*logN)
* 堆排序
*/
public static void heapSort(int[] array) {
createBigHeap(array);
int end = array.length - 1;
while (end > 0) {
swap(array, 0, end);
shiftDown(array, 0, end);
end--;
}
}
private static void createBigHeap(int[] array) {
int parent = (array.length -1 - 1) / 2;
while (parent >= 0) {
shiftDown(array, parent, array.length);
parent--;
}
}
public static void shiftDown(int[] array, int parent, int end) {
int child = parent * 2 + 1;
while (child < end) {
if(child + 1 < end && array[child + 1] > array[child]) {
child ++;
}
if(array[child] > array[parent]) {
swap(array, child, parent);
parent = child;
child = parent * 2 + 1;
}else {
break;
}
}
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
6.3 性能分析
7. 冒泡排序
7.1 原理
在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序
7.2 实现
public static void bubbleSort(int[] array) {
for(int i = 0; i<array.length - 1; i++) {
boolean flag = false;
for(int j = 0; j<array.length -1 -i; j++) {
if(array[j] > array[j+1]) {
swap(array, j, j+1);
flag = true;
}
}
if(!flag) {
break;
}
}
}
7.3 性能分析
8. 快速排序(重要)
8.1 hoare版本 (左右指针法)
思路:
1、选出一个key,一般是最左边或是最右边的。
2、定义一个begin和一个end,begin从左向右走,end从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要end先走;若选择最右边的数据作为key,则需要bengin先走)。
3、在走的过程中,若end遇到小于key的数,则停下,begin开始走,直到begin遇到一个大于key的数时,将begin和right的内容交换,end再次开始走,如此进行下去,直到begin和end最终相遇,此时将相遇点的内容与key交换即可。(选取最左边的值作为key)
4.此时key的左边都是小于key的数,key的右边都是大于key的数
5.将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,此时此部分已有序
单趟动图如下:
实现
/**
* 快速排序
* 时间复杂度:
* 最好:O(N*logN)
* 最坏:O(N^2) 逆序/有序
* 空间复杂度: O(logN)
* 不稳定
* @param array
*/
public static void quickSort(int[] array) {
quick(array, 0, array.length-1);
}
private static void quick(int[] array, int start, int end) {
if(start >= end) return;
int pivot = partitionHoare(array, start, end);
quick(array, start, pivot-1);
quick(array, pivot+1, end);
}
private static int partitionHoare(int[] array, int left, int right) {
int key = array[left];
int i = left; //记住最左边的下标
while (left < right) {
while (left < right && array[right] >= key) {
right--;
}
//right 下标一定是比 key 小的数
while (left < right && array[left] <= key) {
left++;
}
//left 下标一定是比 key 大的数
swap(array, left, right);
}
swap(array, left, i);
return left;
}
8.2 挖坑法
基本思路和Hoare 法一致,只是不再进行交换,而是进行赋值(填坑+挖坑)
实现
private static int partitionHole(int[] array, int left, int right) {
int key = array[left];
while (left < right) {
while (left < right && array[right] >= key) {
right--;
}
array[left] = array[right];
while (left < right && array[left] <= key) {
left++;
}
array[right] = array[left];
}
array[left] = key;
return left;
}
8.3 前后指针法
基本思想:
我们定义两个指针cur和prev,选取key值,cur去遍历小于key的值,对prev++,交换cur与prev值,直至cur遍历完整个数组,prev位置的值一定是比key值小的,即key应处的正确位置
实现
private static int partition(int[] array, int left, int right) {
int prev = left;
int cur = left+1;
while (cur <= right) {
if(array[cur] < array[left] && array[++prev] != array[cur]) {
swap(array, cur, prev);
}
cur ++;
}
swap(array, prev, left);
return prev;
}
8.4 快速排序优化
1. 三数取中法选key
2. 递归到小的子区间时,可以考虑使用插入排序
public static void quickSort(int[] array) {
quick(array, 0, array.length-1);
}
//三数取中
private static int middleNum(int[] array, int left, int right) {
int mid = (left + right) / 2;
if(array[left] < array[right]) {
if(array[mid] < array[left]) {
return left;
}else if (array[mid] > array[right]) {
return right;
}else {
return mid;
}
}else {
if(array[mid] < array[right]) {
return right;
} else if (array[mid] > array[left]) {
return left;
}else {
return mid;
}
}
}
public static void insertSort1(int[] array, int left, int right) {
for(int i = left+1; i<right; i++) {
int tmp = array[i];
int j = i-1;
for(; j >= left; j --) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
break;
}
}
array[j+1] = tmp;
}
}
private static void quick(int[] array, int start, int end) {
//优化1: 插入排序
if(end - start + 1 <= 15) {
insertSort1(array, start, end);
return;
}
//优化2: 三数取中
int index = middleNum(array, start, end);
swap(array, index, start);
int pivot = partition(array, start, end);
quick(array, start, pivot-1);
quick(array, pivot+1, end);
}
优化总结
1. 选择基准值很重要,通常使用几数取中法
2. partition过程中把和基准值相等的数也选择出来
3. 待排序区间小于一个阈值时(例如48) , 使用直接插入排序
8.5 快速排序非递归
快速排序的非递归思路通常借助数据结构(如栈)来模拟递归过程中的函数调用栈。
基本思想如下:
-
选择一个基准元素,将数组划分为两部分,左边的元素都小于等于基准元素,右边的元素都大于等于基准元素,并得到基准元素的最终位置。
-
把待排序的子数组的起始和结束索引存入栈中。
-
当栈不为空时,取出栈顶的一对起始和结束索引。
-
对取出的子数组进行划分操作,得到新的基准位置。
-
如果划分后的左右子数组的长度大于 1,将它们的起始和结束索引再次压入栈中。
-
重复上述过程,直到栈为空,此时数组排序完成。
这样,通过栈来保存需要排序的子数组范围,就避免了递归调用带来的函数栈空间开销,实现了快速排序的非递归版本。
public static void quickSortNor(int[] array) {
int start = 0;
int end = array.length-1;
Stack<Integer> stack = new Stack<>();
stack.add(start);
stack.add(end);
while (!stack.isEmpty()) {
end = stack.pop();
start = stack.pop();
int pivot = partitionHoare(array, start, end);
if(start + 1 < pivot) {
stack.add(start);
stack.add(pivot-1);
}
if(pivot+1 < end) {
stack.add(pivot+1);
stack.add(end);
}
}
}
9.归并排序
9.1 原理-总览
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
9.2 实现
/**
* 归并排序
* 时间复杂度: O(N*logN)
* 空间复杂度: O(logN)
* 稳定性: 稳定
* 目前为止3个稳定的排序: 直接插入排序, 冒泡排序, 归并排序
*/
public static void mergeSort(int[] array) {
mergeSortFun(array, 0, array.length - 1);
}
private static void mergeSortFun(int[] array, int start, int end) {
if (start >= end) {
return;
}
int mid = (start + end) / 2;
mergeSortFun(array, start, mid);
mergeSortFun(array, mid + 1, end);
//合并
merge(array, start, mid, end);
}
//这里的思路和合并两个有序数组相似
private static void merge(int[] array, int left, int mid, int right) {
int s1 = left; //可以不定义, 为了方便理解才定义
int e1 = mid; //可以不定义, 为了方便理解才定义
int s2 = mid + 1;
int e2 = right; //可以不定义, 为了方便理解才定义
//定义一个新的数组
int[] tmpArr = new int[right - left + 1];
int k = 0; //tmpArr 数组的下标
while (s1 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]) {
tmpArr[k++] = array[s1++];
}else {
tmpArr[k++] = array[s2++];
}
}
while (s1 <= e1) {
tmpArr[k++] = array[s1++];
}
while (s2 <= e2) {
tmpArr[k++] = array[s2++];
}
//把排好序的数据拷贝回原来的数组array中
for(int i = 0; i<tmpArr.length; i++) {
array[i+left] = tmpArr[i];
}
}
9.3 性能分析
9.4 非递归版本
归并排序的非递归算法思路主要是通过逐步合并相邻的子序列来实现排序。
以下是归并排序非递归算法的一般步骤:
-
初始化子序列长度为 1。
-
当子序列长度小于数组长度时,执行以下操作:
- 以当前子序列长度为步长,对相邻的子序列进行两两合并。
- 合并时,创建一个辅助数组来存储合并后的结果。
- 将合并后的结果放回原数组。
-
将子序列长度翻倍,重复步骤 2,直到子序列长度大于等于数组长度,此时排序完成。
这种方法从最小的子序列开始,逐步合并,最终完成整个数组的排序,避免了递归调用带来的栈空间开销。
/**
* 归并排序非递归实现
*/
public static void mergeSortNor(int[] array) {
int gap = 1; //每组有几个数据
while (gap < array.length) {
for(int i = 0; i<array.length; i=i+gap*2) {
int left = i;
int mid = left+gap-1; //可能会越界
int right = mid+gap; //可能会越界
if(mid >= array.length) {
mid = array.length-1;
}
if(right >= array.length) {
right = array.length-1;
}
merge(array, left, mid, right);
}
gap*=2;
}
}
10. 计数排序
10.1原理
计数排序(Counting Sort)是一种非比较排序算法。
它的工作原理是:首先找出待排序数组中的最大和最小元素,确定计数范围。然后创建一个计数数组,用于统计每个元素出现的次数。接着,对计数数组进行累加操作,得到每个元素在最终有序数组中的位置信息。最后,根据计数数组将原始数组中的元素放置到正确的位置,从而得到有序数组。
计数排序的优点是:它的时间复杂度为 O(n + k),其中 n 是待排序数组的长度,k 是数组中元素的取值范围。当 k 较小时,它的性能非常出色,而且是稳定的排序算法。
然而,计数排序也有一些局限性:它只适用于整数排序,且当元素的取值范围过大时,可能会导致额外的空间消耗过大。
10.2 实现
/**
* 计数排序
* 使用场景: 指定范围内的数据
* 时间复杂度:O(Max(N,范围))
* 空间复杂度:O(范围)
* 稳定性:稳定
* 这里的写法比较简单, 为不稳定的排序
*/
public static void countSort(int[] array) {
//先获取数据中的最大值和最小值
int minVal = array[0];
int maxVal = array[0];
for(int i = 1; i<array.length; i++) {
if(array[i] < minVal) {
minVal = array[i];
}
if(array[i] > maxVal) {
maxVal = array[i];
}
}
//确定计数数组的长度
int len = maxVal - minVal + 1;
int[] count = new int[len];
//遍历array 数组, 把数据出现的此时记录在 count 数组中
for(int i = 0; i<array.length; i++) {
count[array[i] - minVal]++;
}
//计数数组已经存放了每个数据出现的次数
//遍历计数数组, 把实际的数据写回array数组
int index = 0;
for(int i = 0; i<count.length; i++) {
while (count[i] > 0) {
//这里需要重新写回 array
array[index++] = i+minVal;
count[i] --;
}
}
}
10. 排序总结
海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1. 先把文件切分成 200 份,每个 512 M
2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
3. 进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
其他排序:
1.基数排序
2.桶排序