目录
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 7
int index = middleNum(array,start,end);
swap(array,index,start);
//4 2 3 1 5 6 7
int 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 7
int index = middleNum(array,start,end);
swap(array,index,start);
//4 2 3 1 5 6 7
int 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、桶排序
算法思想:划分多个范围相同的区间,每个子区间自排序,最后合并
计数排序、基数排序、桶排序 都是非基于比较的排序