目录
8、快速排序
8.1、Hoare版
8.2、挖坑法
8.3、前后指针法
9、快速排序优化
9.1、三数取中法
9.2、采用插入排序
10、快速排序非递归
11、归并排序
12、归并排序非递归
13、排序类算法总结
14、计数排序
15、其他排序
15.1、基数排序
15.2、桶排序
8、快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法
基本思想:任取待排序元素序列中的某元 素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
8.1、Hoare版
1. 把第一个值作为基准值 pivot
2. right 从右边走,遇到比 pivot 大的就停下;left 从左边走,遇到比 pivot 小的就停下
3. 交换 left 和 right 的值
4. left 和 right 继续走,直到 left 和 right 相遇
5. 相遇的位置就是要找的位置,把基准值与该位置交换
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 tmp = array[left];int pivot = left;while (left < right) {// 单独的循环 不能一直减到超过最左边的边界while (left < right && array[right] >= tmp) {right--;}while (left < right && array[left] <= tmp) {left++;}swap(array,left,right);}swap(array,pivot,left);return left;}
两个问题:
1. 为什么 array[right] >= tmp 必须带等于号
可能会出现 left 和 right 无限交换的死循环
2. 为什么从 right 先走而不是 left
如果 left 先走可能会出现相遇的是比基准大的数据,最后把大的数据放到了最前面
快速排序总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
最好的情况下:O(N*logN)
最坏情况下:O(N^2) -- 逆序/有序
3. 空间复杂度:O(logN) -- 递归了logN层
最好的情况下:O(logN)
最坏情况下:O(N) -- 逆序/有序
4. 稳定性:不稳定
8.2、挖坑法
1. 把第一个值拿出来作为基准值 tmp,第一个值的位置就是第一个坑
2. right 从右边走,遇到比 pivot 大的就停下,然后把这个值放到上一个坑里,right 就形成了新的坑
3. left 从左边走,遇到比 tmp 小的就停下,然后把这个值放到坑里,left 就是新的坑
4. 循环2、3,直到 left 和 right 相遇
5. 相遇的位置就是要找的位置,把基准值放在这个位置
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 = partitionHole(array,start,end);quick(array,start,pivot-1);quick(array,pivot+1,end);}private static int partitionHole(int[] array, int left, int right) {int tmp = array[left];while (left < right) {// 单独的循环 不能一直减到超过最左边的边界while (left < right && array[right] >= tmp) {right--;}array[left] = array[right];while (left < right && array[left] <= tmp) {left++;}array[right] = array[left];}array[left] = tmp;return left;}
8.3、前后指针法
1. 定义两根指针cur和prev,初始位置如下图
2. cur开始往后走,如果遇到比key小的值,则++prev,然后交换prev和cur指向的元素,再++cur,如果遇到比key大的值,则只++cur
3. 当cur访问过最后一个元素后,将key的元素与prve访问的元素交换位置。cur访问完整个数组后的各元素位置如下图
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;
}
总结:
1. Hoare
2. 挖坑法
3. 前后指针法
这3种方式 每次划分之后的前后顺序 有可能是不一样的
9、快速排序优化
优化的出发点:减少递归的次数
9.1、三数取中法
既然有序数组或者有序数组片段会使效率下降,我们就可以让基准值每次都取大小靠中的数,然后在进行快速排序这样就可以避免了。但不是完全避免,只是减少了最坏情况出现的概率,最坏情况还是O(n²),但有效提升了运行效率,主要提升的部分是数组中有序的数组片段,减少了循环次数。
// 求中位数的下标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 {//array[left] > array[right]if(array[mid] < array[right]) {return right;}else if(array[mid] > array[left]) {return left;}else {return mid;}}}private static void quick(int[] array,int start,int end) {if(start >= end) {return;}//1 2 3 4 5 6 7int index = middleNum(array,start,end);swap(array,index,start);//4 2 3 1 5 6 7int pivot = partition(array,start,end);quick(array,start,pivot-1);quick(array,pivot+1,end);}
9.2、采用插入排序
往往一棵树的最后两层的结点占整棵树的绝大多数,所以当递归到一定深度时,采用直接插入排序
public static void insertSort(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) {if(start >= end) {return;}if(end - start + 1 <= 15) {insertSort(array, start, end);return;}//1 2 3 4 5 6 7int index = middleNum(array,start,end);swap(array,index,start);//4 2 3 1 5 6 7int pivot = partition(array,start,end);quick(array,start,pivot-1);quick(array,pivot+1,end);}
10、快速排序非递归
1. 先调用partition方法找到基准
2. 基准左边和右边有没有2个及以上个元素,有就把下标放到栈中3. 判断栈空不空,不空出栈2个,第一个是新的end,第二个是新的start
4. 栈不为空时,循环执行上述1.2.3
public static void quickSortNor(int[] array) {int start = 0;int end = array.length-1;Stack<Integer> stack = new Stack<>();int pivot = partition(array,start,end);if(pivot > start+1) {stack.push(start);stack.push(pivot-1);}if(pivot+1 < end) {stack.push(pivot+1);stack.push(end);}while (!stack.isEmpty()) {end = stack.pop();start = stack.pop();pivot = partition(array,start,end);if(pivot > start+1) {stack.push(start);stack.push(pivot-1);}if(pivot+1 < end) {stack.push(pivot+1);stack.push(end);}}}
11、归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
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) {// s1,e1,s2,e2 可以不定义,这样写为了好理解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];}}
两个有序数组合并为一个有序数组代码:
public static int[] mergeArray(int[] arrayl,int[] array2) {// 注意判断参数int[] tmp = new int[array1.length+array2.length];int k = 0;int s1 = 0;int el = array1.length-1;int s2 = 0;int e2 = array2.length-1;while (s1 <= el && s2 <= e2) {if(array1[s1] <= array2[s2]) {tmp[k++] = array1[s1++];}else {tmp[k++]=array2[s2++];}}while (s1 <= el) {tmp[k++] = array1[s1++];}while (s2 <= e2) {tmp[k++]= array2[s2++];}return tmp;
}
归并排序总结
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)递归调用栈空间:O(logN)
合并操作空间:O(N)
4. 稳定性:稳定
海量数据的排序
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1. 先把文件切分成 200 份,每个 512 M
2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
3. 进行2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了(在外部存储进行)
12、归并排序非递归
1. 找到 left、mid、right 的位置和关系,然后调用merge合并
2. 定义 gap 表示当前的分组是每组几个数据
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;// mid、right 可能会越界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;}
13、排序类算法总结
14、计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中具体实现:
1.申请一个数组count,大小为待排序数组array的范围 M
2.遍历待排序数组array,把数字对应的count数组的下标内容进行++
3.遍历计数数组count 写回到待排序数组array,此时需要注意写的次数和元素值要一样
4. 最后数组array中存储的就是有序的序列
public static void countSort(int[] array) {//求数组的最大值 和 最小值 O(N)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数组 把数据出现的次数存储到计数数组当中 O(N)for (int i = 0; i < array.length; i++) {count[array[i]-minVal]++;}//计数数组已经存放了每个数据出现的次数//遍历计数数组 把实际的数据写回array数组 O(M) M表示数据范围int index = 0;for (int i = 0; i < count.length; i++) {while (count[i] > 0) {//这里需要重写写回array 意味着得从array的0位置开始写array[index] = i+minVal;index++;count[i]--;}}}
计数排序的特性总结
1.计数排序是非基于比较的排序2. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限;计数排序的场景:指定范围内的数据
3. 时间复杂度:O(MAX(N,M)) M表示数据范围
4. 空间复杂度:O(M)
5. 稳定性:稳定;上述代码是不稳定的写法
15、其他排序
15.1、基数排序
基数排序(Radix Sort)是一种非比较型的排序算法,它通过逐位比较元素的每一位(从最低位到最高位)来实现排序。基数排序的核心思想是将整数按位数切割成不同的数字,然后按每个位数分别进行排序。基数排序的时间复杂度为 O(d*(n+r)),其中 n 为元素个数,d 是最大数字的位数,r 为基数(桶的个数)
时间复杂度分析:
分配依次将每个数放到对应的桶中 O(n)
收集依次将每个桶里的元素拿出来 O(r) (桶里的元素是用链表连接的)
每轮:分配+收集 O(n+r)
如果最大的数字有d位,就需要排d轮
所以时间复杂度为:O(d*(n+r))
15.2、桶排序
算法思想:划分多个范围相同的区间,每个子区间自排序,最后合并
计数排序、基数排序、桶排序 都是非基于比较的排序