目录
排序(sorting)
1.概念
1.1排序
1.2稳定性
1.3算法时间复杂度、空间复杂度
两者都有最好情况、最坏情况、平均情况
1.4区间表示法
元素类型使用 long 类型、下标类型使用 int 类型。long[] array,int from,int to
左闭右闭 size = from - to + 1;左闭右开size= to - from。
2.七大基于比较的排序-总览
3.插入排序
3.1直接插入排序
1、记取出来进行排序的这个数为k ,插入合适的位置,依次向前比较,所谓合适的位置就是第一次遇到 k >= x 的位置。
2、代码实现:
public static void insertSort(long[] array){
// 一共要取多少个元素来进行插入过程(无序区间里有多少个元素)
for(int i = 0;i < array.length - 1;i++){
// 有序区间:[0,i];无序区间:[i + 1,n)
// 取出无序区间的第一个数
long k = array[i + 1];
// 从后往前,遍历有序区间,找到第一次k >= array[j] 的位置
int j;
for(j = i;j >=0 && k < array[j];j--){
array[j + 1] = array[j]; // 不符合条件的情况
}
array[j + 1] = k;
}
}
3、时间复杂度: 最好O(n);平均和最坏O(n^2); 空间复杂度:O(1);具备稳定性
插排和冒泡从性能分析和稳定性方面完全一致。但插排远快于冒泡的,所以冒泡基本没人用。
3.2希尔排序(Shell sort)
1.在插排的基础上做优化。先预排序 -> 虽然不能让数据完全有序,但可以让数据解决有序,提高跨度的插排。
逻辑上按照固定长度的间隔做分组,比如用 3 间隔,就可以把10 个数分成3 组。
3 1 0 4 2 5 7 8 6 9;3 4 7 9看成一组,1 2 8看成一组,0 5 6 看成一组。各自在各自的分组内部做插排。
2.如何进行各自分组的插排
//gap:带间隔 / 一共多少组
public static void insertSortWithGap(long[] array,int gap){
//外围的循环次数是 n - gap 次
for(int i = 0; i < array.length - gap;i++){
// 一共有gap 个分组
//有序:[0.i + gap];无序:[i + gap,n)
long k = array[i + gap];
int j;
// j 下标只去找同一组的元素比较,所以每次跳过间隔
for(j = i;j >= 0 && k < array[j];j = j - gap){
array[j + gap] = k;
}
}
}
使用比较大的gap,gap大小减少,直到gap 趋于1,当gap 为1 的时候就是插排,结果就是有序的。通常情况下,gap = array.length / 2 、 gap = gap / 2;收敛为1
3.希尔排序
public static void shellSort(long[] array){
int gap = array.length / 2;
while(gap > 1){
insertSortWithGap(array,gap);
gap = gap / 2;
}
//最后再执行一次插排
insertSort(array);
}
时间复杂度:O(n^1.3);不具备稳定性(相等的元素可能分不到一个组里)
4.选择排序
4.1直接选择排序
每次遍历无序区间,找到最大的数的位置,把他和无序区间最后一个数进行交换,其实就是保证最大的数永远出现在最后,下次再去无序区间找下一个最大的数,交换到最后。也可以每次选最小的数放前面,或者选出一个最大一个最小往两边放。
public static void selectSort(long[] array){
//每次选择出最大的数,放到最后去
// 一共要选择出 array.length - 1 个数
for(int i = 0;i < array.length - 1;i++){
// 通过遍历无序区间只找打最大的数所在的位置就可以了,不换
// 无序区间[0,array.length - i)
int maxIndex = 0;//假设最大数放在0 号位置
for(int j = 1;j < array.length - i;j++){
if(array[j] > array[maxIndex]){
// 说明无需区间找到了新的最大的数
// 所以记录下最大数的下标
maxIndex = j;
}
}
// 遍历完之后,无序区间的最大的数就放在maxIndex所在下标处
// array[maxIndex] 是最大数
// 交换[maxIndex] 和 无序区间最后一个元素[array.length - i -1]
swap(array,maxIndex,array.length - i- 1);
}
}
时间复杂度:O(n^2);不具备稳定性
4.2堆排序
选出无序区间的最大的元素交换到最后面去——找最值(用堆)——把无序区间逻辑上看成堆,构建一个大堆。
代码实现:
public static void heapSort(long[] array){
//1.建立大堆
createBigHeap(array);
//2.遍历n - 1次
for(int i = 0 ;i < array.length - 1;i++ ){
// 2.1交换之前的无序区间[0,n - 1)
swap(array,0,array.length - i -1);
// 交换之后的无序区间[0,n - i - 1),元素个数n - i- 1 个
// 2.2 对堆的[0]进行向下调整,堆里的元素个数就是无序区间的元素个数
shiftDown(array,array.length - i - 1,0);
}
}
// 向下调整
private static void shiftDown(long[] array,int size,int index){
while(2 * index + 1 < size){
int maxIndex = 2 * index + 1;
int right = maxIndex + 1;
if(right < size && array[right] > array[maxIndex]){
maxIndex = right;
}
if(array[index] >= array[maxIndex]){
return;
}
swap(array,index,maxIndex);
index = maxIndex;
}
}
// 建堆操作
private static void createBigHeap(long[] array){
//从最后一个元素的双亲开始
for(int i = (array.length - 2) / 2;i >= 0;i--){
shiftDown(array,array.length,i);
}
}
时间复杂度:O(n*log(n));空间复杂度:O(1);不具备稳定型(因为堆的调整过程中,如果相等的元素在不同的子树中,无法控制)
5.交换排序
5.1冒泡排序
1、冒泡排序(bubble sort):减治算法。减:每次解决一个问题之后,问题规模在减少。治:采用相同的方式处理相同的问题。
2、对 n 个的数组做冒泡排序,每次经过冒泡,都可以将一个最大的数冒泡到最后去,n -> n - 1 -> n - 2 -> 1... -> 0
3、代码实现:
//冒泡排序
public class Sort {
// 对整个数组做排序
public static void bubbleSort(long[] array){
// 一个数组经过 array.length - 1 次 这个数组就有序了
for(int i = 0;i< array.length;i++){
// 每次将最大的数经过冒泡的方式,放到最后
// 整个数组 = 【无序区间】【有序区间】
// 无序区间:[0,array.length - i]
// 有序区间:[array.length - i,array.length]
for(int j = 0;j < array.length - i - 1;j++){
if(array[j] > array[j + 1]){
swap(array,j,j + 1);
}
}
}
}
private static void swap(long[] array, int i, int j){
long temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
4、测试的完备性问题——对于排序来说:
1.正常情况:[9, 1, 3, 5, 2, 4, 8 ,0 ,7]
2.一些特殊情况——边界法:
数组中一个元素都没有[];数组中的所有元素都一样[3, 3, 3, 3, 3, 3];数组已经有序了[1, 2, 3, 4, 5, 6, 7];数组是逆序[7, 6, 5, 4, 3, 2, 1];数组中的元素特别大的情况
5、时间复杂度:最好情况是O(n);平均、最坏情况是O(n^2)
空间复杂度:O(1) 具备稳定性
5.2快速排序
1、快速排序过程:
1.选择一个元素 pivot(支点)(如何选择不重要),通过遍历的方式,比较给定元素和区间内其他元素的大小关系。
2.在遍历期间,通过算法设计,让区间划分为三段:pivot、左边小于等于pivot、右边大于等于pivot。注意:pivot 不一定刚刚好在中间,因为 pivot 是任意选择的。两边区间内的元素互相之间的顺序没有任何保证。
3.第二步完成之后,pivot 就是处于最终有序后应该在的位置,认为 pivot 已经排好顺序了
4.对左右两个小区间进行排序(可能其中一个区间里面一个元素都没有)
最开始,可能我们要对10个元素的区间进行排序,经过一番处理后。变成两个小问题(问题性质一样,但数据规模在减小),变成了对 7 个元素 和 2 个元素的区间分别进行排序。
2、快速排序核心思路总结:
0.如果区间内的元素个数 <= 1个,则不用做任何处理,因为天然有序。
1.从区间中(这里区间不代表整个数组,所以需要通过[from, to] 来限定)中挑选一个pivot ,挑选方式随意。我们暂时选用区间最右边的一个元素。
2.遍历区间,将每个元素都和 pivot 进行比较,并且进行必要的位置移动,使得区间遍历完成之后满足区间被分为三个部分,[ <= pivot ] [ pivot ] [ >=pivot ]。整个过程一般被称为 partitoin(分割区间)。注意:partition 只是快排中的一个小步骤而已
3.继续对左右两个小区间,按照同样的方式进行处理。
下面讨论的区间,很大可能性是处于原始数组中间的一段区间。array 中的[from,to] from 很大概率不是 0,虽然一开始是 0;同理,to 很大概率也不是 array.length - 1 ,虽然一开始是。
1.第一种 partition
两边向中间的逼近
整个待区分的区间,在分区的过程中,大体又可以分为 4 个小区域;除了pivot 之外,应该还有 3 个区域,分别是 1.所有 <= pivot 的元素;2.所有 >= pivot 的元素;3.所有还未和 pivot 比较过的元素。
建议:当array[to] = pivot 时,先从 array[left] 开始和 pivot 比较
比较结果:array[left] <= pivot:直接让 left 朝右走一步,left++;继续比较array[left]
array[left] > pivot:array[left] 和 pivot 的比较结束,该让array[right] 和 pivot
比较,当array[right] >= pivot 时,right--;
array[left] > pivot; array[right] < pivot,这个时候就卡住了,所以我们把right 和 left 进行交换
交换完成之后仍然保持:【from , left) <= pivot;【right , to】 >= pivot;【left , right) 未参与比较。
然后,再让array【left】 参与比较
比较结果:array[left] <= pivot:直接让 left 朝右走一步,left++;继续比较array[left]
array[left] > pivot:array[left] 和 pivot 的比较结束,该让array[right] 和 pivot
比较,当array[right] >= pivot 时,right--;
实际上就是:先让array[left] 参与比较,直到 array【left】 > pivot;再让array【right】 参与比较,直到array【right】 < pivot;当两边都卡住时,交换【left】和【right】处的元素,进行这样的循环。出口:【left,right)区间里没有元素了,也就是 right = left 时结束循环,这个时候我们的partition 就结束了。所以只要 left < right 那么我们的循环就继续。
因为最后我们要把pivot 放在中间,所以最后一步当 left = right 时,把left 位置的元素和 pivot 进行交换。
2.partition代码:
/**
* 以区间最右边的元素 array[to] 作为pivot。遍历整个区间,从 from 到 to
* 移动必要的元素,进行分区
* @param array
* @param from
* @param to
* @return
*/
private static int partitionMethodA(long[] array,int from,int to){
// 1.先找出pivot
long pivot = array[to];
// 2.定义left 和 right 两个下标
int left = from;
int right = to;
//[from,left) 都是 <= pivot
//[left,right) 都是未参与比较的
//[right,to] 都是 >= pivot 的
//循环,保证每个元素都参与了和 pivot 的比较
// 也就是,只要[left,right)区间内还有元素,循环就应该继续
while (left < right){
//左边先比较
// 随着 left 在循环过程中一直在++,left < right 的条件不能一直保证
// 所以要时刻进行left < right 的保证
// 且 得现有 left < right 后面的比较才有意义
while(left < right && array[left] <= pivot){
left++;
}
//循环停止时,说明array[left] > pivot
while (left < right && array[right] >= pivot){
right--;
}
//循环停止时,说明array[right] < pivot
// 两边都卡住时,交换[left] 和 [right] 位置的元素
long t = array[left];
array[left] = array[right];
array[right] = t;
}
// 走到这里,说明 left == right
// 待比较区间中已经没有元素了,都已经在各自应该的位置上了
long t = array[to];
array[to] = array[left];
array[left] = t;
//返回 pivot 最终所在下标
return left;
}
3.快速排序代码:
public class Sort {
public static void quickSort(long[] array){
quickSortRange(array, 0, array.length - 1);
}
//为了代码书写方便,我们选择使用左闭右闭的区间表示形式
// 让我们对 array 中从 from 到 to 的位置进行排序,其他地方不用管
// 其中, from 和 to 都算在区间中
// 左闭右闭的情况下,区间内的元素个数 = to - from + 1
private static void quickSortRange(long[] array,int from,int to){
if(to - from + 1 <= 1){
//区间中元素个数 <= 1个,自然有序
return;
}
//挑选出区间中最右边的元素array[to]
int pi = partitionMethodA(array,from,to);
// 小于等于 pivot 的元素所在的区间如何表示 array,from,pi - 1
// 大于等于pivot的元素所在的区间如何表示 array,pi + 1,to
// 按照分治算法的思路,使用相同的方式,处理相同性质的问题,只是问题的规模在变小
quickSortRange(array,from,pi - 1); // 针对小于等于 pivot 的区间做处理
quickSortRange(array,pi + 1,to); // 针对大于等于 pivot 的区间做处理
}
private static int partitionMethodA(long[] array,int from,int to){
//此处省略
}
}
3.快速排序的性能分析
1.针对partition 这个单独的步骤,它的时间复杂度是多少?
1)n 所代表的数据规模是什么? 该区间的元素个数 区间中的每个元素都和partition 做过一次(并且只有一次)比较,其他的工作,几乎可以忽略。
时间复杂度:O(n) 空间复杂度:O(1)
2.快排的时间复杂度
把快排看成是一个二叉树
时间复杂度:最好、平均:O(n * log(n) ) 最坏:O(n * n)
时间复杂度:partition 的时间复杂度O(n) * 树的高度
方法的执行过程中,要避开调用栈,调用栈是对一段内存空间的抽象。调用栈开辟的越多,对内存的使用越多。调用栈的多少是快排唯一和 n 有关的数据。所以空间复杂度主要看 这个关系。最终还是表现为二叉树的高度,在log(n) 到 n 之间变化。
空间复杂度:最好、平均:O( log(n) ) 最坏:O(n)
空间复杂度:partition 的空间复杂度O(1) * 树的高度
快排是唯一一个空间复杂度也分情况讨论的排序算法
稳定性:保证不了,因为没有可以在做到O(n)的情况下,还能保证稳定性的 partition 算法。
4.第二种partition
还是分为三种情况,但比第一种的性能略好一些,是一种挖坑。
第二种 partition 的代码:
private static int partitionMethodB(long[] array,int from,int to){
long pivot = array[to];
int left= from;
int right =to;
while(left < right){
while (left < right && array[left] <= pivot){
left++;
}
array[right] = array[left];
while (left < right && array[right] >= pivot){
right--;
}
array[left] = array[right];
array[left] = pivot;
}
return left;
}
5.第三种partition
【from,s) 元素 <= pivot 【s,i)元素 >= pivot 【i,to)元素待比较
i 的遍历范围 【from,to)
array【i】 < pivot:交换【i】和【s】的元素,同时i++;s++;
array【i】 > pivot:i++,s不动
array【i】 < pivot:交换【i】和【s】的元素,同时i++;s++;
走到 i = to 时,说明走完了,交换 i 和 s 的值
第三种 partition 代码实现:
private static int partitionMethodC(long[] array,int from,int to){
int s = from;
long pivot = array[to];
for(int i = from;i < to;i++){ //遍历【from,to)
if(array[i] < pivot){
long t = array[i];
array[i] = array[s];
array[s] = t;
s++;
}
}
array[to] = array[s];
array[s] = pivot;
return s;
}
6.partition 进阶
希望完成partition 之后,我们的区间被分成3 部分:【<pivot】【==pivot】【>pivot】
与之前不同的是,把 == pivot 的全部聚集在一起
partition 的过程中,应该 4 各部分:小于pivot、等于pivot、大于pivot和未参与比较的
array【i】 < pivot:交换【i】和【s】;s++;i++
array【i】 == pivot:i++;
array【i】 > pivot:交换【i】和【g】;g--;i不动
这里需要返回类型有点不同:需要同时返回两个位置1、< pivot的尾巴 2、> pivot 的开头。
java的方法是无法一次性返回两个值:因此1)专门定义个类用于返回 2)两个位置都是int 型(类型相同)int[] 作为返回类型,我们的 partition 一定返回一个int [] ,元素一定只有两个,一个是小于pivot 的结尾,一个是 大于pivot 的开头。
// partition 进阶
private static int[] partitionMethodD(long[] array,int from,int to){
int s = from;
int i = from;
int g = to;
long pivot = array[to];
// 只要有元素还没有比较过,循环继续
while (g - i + 1 > 0){
if(array[i] == pivot){
i++;
} else if(array[i] < pivot){
swap(array,i,s);
i++;
s++;
} else {
swap(array,i,g);
g--;
}
}
return new int[] {s - 1,g + 1};
}
7.快排的几个常见优化手段
1.前提结论:再待排序区间元素比较少的情况下,快排的速度低于插排。所以,待排序区间的元素低于一个阈值(比如说取一个20),直接使用插排完成排序动作。
代码体现:
// 【from,to】 是左闭右闭的
private static void insertSortRange(long[] array,int from,int to){
int size = to - from;
for(int i = 0;i < size;i++){
// 有序区间【from,from + 1】
// 无序区间【from + i + 1,to】
// 选中的无序区间的第一个元素 array【from + i + 1】
long key = array[from + i + 1];
int j;
for(j = from + i;j >= from && array[j] > key;j--){
array[j + 1] = array[j];
}
array[j + 1] = key;
}
}
private static void quickSortRange(long[] array,int from,int to){
// 待排序元素个数<= 1 的情况下,什么都不需要做
if(to - from + 1 <= 1){
return;
}
//待排序元素个数 <= 20 的情况下,使用插排完成排序
if(to - from + 1 <= 20){
insertSortRange(array,from,to);
return;
}
int[] indices = partitionMethodD(array,from,to);
int lessIndex = indices[0];
int greatIndex = indices[1];
//小于pivot的区间【from,lessIndex】
quickSortRange(array,from,lessIndex);
//大于pivot的区间【greatIndex,to】
quickSortRange(array,greatIndex,to);
}
public static void quickSort(long[] array){
quickSortRange(array, 0, array.length - 1);
}
2.partition 上的算法优化(比如,把 == pivot 的提前找出来),其实可以同时选择多个基准值,比如三个p1 < p2 < p3,整个区间就分为了7个部分。
3.选择 pivot 的方式上进行优化
我们选择是区间的最右边(最左边),最大的缺点是,如果区间已经有序或逆序的情况下,会称为最坏的情况。
选择基准值的方式,可以优化成:1.随机选取法:每次随机一个位置,把这个位置作为基准值。生成随机数在计算机本身中就是最高成本操作。虽然最坏的情况仍然存在,但落在最坏情况的期望降低了。2.几个数中选择基准值。暂用三数取中法——取区间的最开始、最中间、最结尾分别取这三个数。取这三个数大小上是中间的那个值作为基准值。这样第一就避免了最坏情况的发生,第二有序、逆序避免成为了最坏情况。
6.归并排序
也是一种分治算法的思想,目前介绍的是两路归并的方式
左边的区间还可以再细分,右边也可以再细分。
1.归并排序的基本思路:
long[] array,int from,int to ,这里使用【from,to)左闭右开的形式,代码更简单
情况一:如果待排序区间已经有序(区间内的元素个数 <= 1),则排序操作可以直接返回
情况二:其他
1.确定中间位置的下标 mid mid = from + (size / 2)。所以【from,to)的区间,被我们从逻辑上视为是左右两个小区间组成的【from,mid)和 【mid,to)
2.然后,对左右两个小区间使用相同的方式,进行排序
3.当左右两个小区间已经有序时,执行合并两个有序区间的操作,得到一个最终的有序大区间
代码表示:
public class MergeSort {
public static void mergeSort(long[] array) {
mergeSortRange(array,0,array.length);
}
private static void mergeSortRange(long[] array,int from,int to){
int size = to - from;
// 情况1:如果区间内的元素个数小于等于 1 个什么都不用干
if(size <= 1){
return;
}
// 其他情况
// 1.找到区间中间位置的下标
int mid = from + (size / 2);
//2.优先对左【from,mid)和右【mid,to)两个小区间先进行排序
//(使用同样的方式处理:分治思想)
mergeSortRange(array,from,mid);
mergeSortRange(array,mid,to);
// 3.有了两个分别各自有序的小区间【from,mid)和【mid,to)
// 通过一定方式,将【from,mid)[mid,to)合并到【from,to)都是有序的
// 两个有序数组合并到一起(需要放回原地)
merge(array,from,mid,to);
}
private static void merge(long[] array, int from, int mid, int to) {
//先计算出来额外空间需要多少个,也就是计算两个空间合起来多大
int size = to - from;
//申请一个额外的数组,作为临时保存的地方
long[] other = new long[size];
int left = from; //左边小区间下标
int right = mid; // 右边小区间下标
int dest = 0; // 临时空间下标
// 只要左右两个小区间还有元素要参与比较
while (left < mid && right < to){
if(array[left] <= array[right]){
other[dest] = array[left];
dest++;
left++;
} else {
other[dest] = array[right];
dest++;
right++;
}
}
//其中一个区间的元素取完了,另一个区间一定还有元素
// 把剩余的元素都放入other中
while (left < mid){
other[dest++] = array[left++];
}
while (right < to){
other[dest++] = array[right++];
}
// 把other 中的有序元素,放回array 中,注意下标问题
for(int i = 0;i < size;i++){
//array的下标基准是从【from】开始,other 下标的基准是从【0】开始
array[from + i] = other[i];
}
}
2.归并排序的性能分析
merge (合并有序区间)操作,时间复杂度:O(n);空间复杂度:O(n)
归并排序时间复杂度:O(n * log(n) )
归并排序空间复杂度:O(n)
稳定性:具备
7. 小结
按照不同的分类标准,划分这些排序。
1.具备稳定性的排序:冒泡、排序、归并
2.平均情况下,执行速度分成两组:
1)慢:冒泡、插排、选择——排序的元素个数在 10万 级别左右
2)快:快排、归并、希尔、堆排——个数在 1亿 级别
3.空间角度:
1)空间复杂度是O(1):冒泡、插排、希尔、选择、堆排
2)空间复杂度是O(log(n))~ O(n):快排
3)空间复杂度是O(n):归并
4)最好情况下(如果数组有序情况下),有两种排序可以达到O(n)的时间复杂度:冒泡、插排
5)属于减治算法(每次问题规模减少1):冒泡、插排、希尔、选择、堆排
属于分治算法(把问题分成多个子问题分别处理):快排、归并
6)外部排序:针对二级存储(硬盘)上的数据进行排序(特点:数据很大。一般认为内存放不下)只有归并支持外部排序。