一.排序算法基本概念
1.常见排序算法
1)O(n^2)的基础排序
选择排序,插入排序,希尔排序,冒泡排序等
2)O(nlogn)的排序
归并排序,快速排序,堆排序等
2.一些概念
1)稳定排序:对于待排序每个元素,假设a=b,排序前a在b前面,那么排序后a也在b前面,称之为稳定的
非稳定排序:与稳定排序相反,排序后相对位置可能会发生变化
2)原地排序:排序过程不申请多余的存储空间
非原地排序:与原地排序相反,需要申请额外的空间来辅助
二.常见排序算法
1.选择排序
时间主要花费在
内层for循环:
比较:n-1次排序,每次n-i-1次比较,花费时间n(n-1)/2;
外层for循环:
交换:0-n-1次
赋值:0-3(n-1)次
故
最差,平均时间复杂度都是O(N^2)
package exercise1;
/**
选择排序
*/
public class SelectionSort1 {
public static void selectionSortVersion1(int[] array){
/*时间复杂度O(n^2),空间复杂度O(1),原地排序,非稳定排序
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return;
}
int length = array.length;
int minIndex,temp;
//从无序区间不断挑选出最小值,共进行n-1次
for (int i = 0; i < length - 1; i++) {
//初始假设最小元素是无序区间的第一个元素
minIndex = i;
//共进行n-i-1次
for (int j = i + 1; j < length; j++) {
if(array[j] < array[minIndex]){
minIndex = j;//更新最小值元素下标
}
}
//把此轮排序寻找到的最小值元素与初始假设最小值元素交换
temp = array[i];
array[i] = array[minIndex];
array[minIndex] = temp;
}
}
public static boolean isSorted(int[] array){
/*测试排序算法的准确性
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return false;
}
for (int i = 0; i < array.length - 1; i++) {
if(array[i] > array[i + 1]){
return false;
}
}
return true;
}
public static void main(String[] args) {
int[] array1 = null;
int[] array2 = new int[]{1};
int[] array3 = new int[]{1,3,7,21,300,401};
int[] array4 = new int[]{999,888,777,10,7,1,-37};
int[] array5 = new int[]{3,1,4,10,2,6,0,21,17,36};
int[] array6 = new int[]{7,0,3,18,0,1,7,21,9};
int[] array7 = new int[]{666,666,666,666};
int[] array8 = new int[]{-5,7,12,-100,7,0,8,-5,-10};
SelectionSort1.selectionSortVersion1(array1);
SelectionSort1.selectionSortVersion1(array2);
SelectionSort1.selectionSortVersion1(array3);
SelectionSort1.selectionSortVersion1(array4);
SelectionSort1.selectionSortVersion1(array5);
SelectionSort1.selectionSortVersion1(array6);
SelectionSort1.selectionSortVersion1(array7);
SelectionSort1.selectionSortVersion1(array8);
System.out.println("排序结果如下---------------->");
System.out.println(SelectionSort1.isSorted(array1));
System.out.println(SelectionSort1.isSorted(array2));
System.out.println(SelectionSort1.isSorted(array3));
System.out.println(SelectionSort1.isSorted(array4));
System.out.println(SelectionSort1.isSorted(array5));
System.out.println(SelectionSort1.isSorted(array6));
System.out.println(SelectionSort1.isSorted(array7));
System.out.println(SelectionSort1.isSorted(array8));
}
}
2.插入排序
特点:
1)插入排序的第二层循环可提前终止,理论上会比选择排序更快
2)对于顺序数组,插入排序变为O(n),内层循环每次只比较一次即可进行下层循环(无赋值操作)
3)对于近乎有序的数组,插入排序性能远远优先于选择排序,甚至会比nlogn级别的算法更快。
4)对于完全逆序的数组,插入排序性能最差
时间主要花费在内层for循环的比较和赋值
外层for循环:
循环n-1次
内层for循环:
最差(前面元素一直大于待插元素)循环i次,共循环1+2+…+(n-1)=n(n-1)/2次,比较n(n-1)/2次,交换n(n-1)/2次,赋值3*n(n-1)/2次,即O(n^2)
平均也是O(n^2)
最好是O(n)
故
最差,平均时间复杂度都是O(N^2)
package exercise1;
/**
插入排序,把未排序的元素一个一个地插入到有序的集合中,
插入时把有序集合从后向前扫一遍,找到合适的位置插入
*/
public class InsertionSort2 {
public static void insertionSortVersion1(int[] array){
/*最差,平均时间复杂度O(n^2),最好时间复杂度O(n),
空间复杂度O(1),原地排序,稳定排序
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return;
}
int length = array.length;
int cur,temp;
//第一个元素认为已排好序,从第二个元素开始插入排序即可
for (int i = 1; i < array.length; i++) {
//备份待插元素值
cur = array[i];
//当满足j >= 0 && array[j] > cur将会一直交换值
for (int j = i - 1; j >= 0 && array[j] > cur; j--) {
temp = array[j];
array[j] = cur;
array[i] = temp;
}
//退出内层for循环时即cur当前位置就是待插入的位置
}
}
public static boolean isSorted(int[] array){
/*测试排序算法的准确性
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return false;
}
for (int i = 0; i < array.length - 1; i++) {
if(array[i] > array[i + 1]){
return false;
}
}
return true;
}
public static void main(String[] args) {
int[] array1 = null;
int[] array2 = new int[]{1};
int[] array3 = new int[]{1,3,7,21,300,401};
int[] array4 = new int[]{999,888,777,10,7,1,-37};
int[] array5 = new int[]{3,1,4,10,2,6,0,21,17,36};
int[] array6 = new int[]{7,0,3,18,0,1,7,21,9};
int[] array7 = new int[]{666,666,666,666};
int[] array8 = new int[]{-5,7,12,-100,7,0,8,-5,-10};
InsertionSort2.insertionSortVersion1(array1);
InsertionSort2.insertionSortVersion1(array2);
InsertionSort2.insertionSortVersion1(array3);
InsertionSort2.insertionSortVersion1(array4);
InsertionSort2.insertionSortVersion1(array5);
InsertionSort2.insertionSortVersion1(array6);
InsertionSort2.insertionSortVersion1(array7);
InsertionSort2.insertionSortVersion1(array8);
System.out.println("排序结果如下---------------->");
System.out.println(InsertionSort2.isSorted(array1));
System.out.println(InsertionSort2.isSorted(array2));
System.out.println(InsertionSort2.isSorted(array3));
System.out.println(InsertionSort2.isSorted(array4));
System.out.println(InsertionSort2.isSorted(array5));
System.out.println(InsertionSort2.isSorted(array6));
System.out.println(InsertionSort2.isSorted(array7));
System.out.println(InsertionSort2.isSorted(array8));
}
}
优化1:一轮排序过程中,我们保留的始终有待排元素的副本。故没有必要每次检查到比待排元素大就立即交换,只需要把比他大的元素顺序后移即可
一次比较,三次赋值可以变为一次比较一次赋值
public static void insertionSortVersion2(int[] array){
/*时间复杂度O(n^2),空间复杂度O(1),原地排序,稳定排序
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return;
}
int length = array.length;
int cur,before;
//第一个元素认为已排好序,从第二个元素开始插入排序即可
for (int i = 1; i < array.length; i++) {
cur = array[i];
before = i - 1;
while(before >= 0 && array[before] > cur){
before--;
}
//退出while循环时说明当前before以满足元素<=cur
//故腾出位置给cur
//注意需要从后边开始移动,否则从前面会发生覆盖
for (int j = i - 1; j >= before+1; j--) {
nums[j + 1] = nums[j];
}
//插入位置应是cur+1
array[before + 1] = cur;
}
}
或者
public static void insertionSortVersion1(int[] array){
/*时间复杂度O(n^2),空间复杂度O(1),原地排序,稳定排序
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return;
}
int length = array.length;
int cur,before;
//第一个元素认为已排好序,从第二个元素开始插入排序即可
for (int i = 1; i < array.length; i++) {
//备份待插元素值
cur = array[i];
//向前比较过程中比cur大的元素顺序后移
for (before = i - 1; before >= 0 && array[before] > cur; before--) {
array[before + 1] = array[before];
}
//退出内层for循环时即before+1当前位置就是待插入的位置
array[before + 1] = cur;
}
}
注:这两种代码其实是等价的
优化2:
利用待排元素之前的所有元素都是有序的,那么可以考虑二分插入排序
public static void insertionSortVersion1(int[] array){
/*时间复杂度O(n^2),空间复杂度O(1),原地排序,非稳定排序
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return;
}
int length = array.length;
int cur,before,left,right,mid;
//第一个元素认为已排好序,从第二个元素开始插入排序即可
for (int i = 1; i < array.length; i++) {
//备份待插元素值
cur = array[i];
//在向前比较过程中可以采用二分法
left = 0;
right = i - 1;
mid = 0;
while(left <= right){
//注意,由于运算符优先级问题一定要加括号((right - left) >> 1);
mid = left + ((right - left) >> 1);
if(cur < array[mid]){
right = mid - 1;
}else if(cur >= array[mid]){ //必须对等于进行判断,为了和while循环后面代码兼容
left = mid + 1;
}
//上述必须对cur == array[mid]等于进行判断,而不是在==的时候break出去,
// 为了和while循环后面代码兼容
//因为后边代码会把left到i-1的元素顺序后移,但是如果是因为==而退出循环的,那么就会多移动元素
}
//顺序后移动需要先移后面的,否则元素会出现覆盖!!
//*****注:正序或者逆序的情况兼容,要求只能用left判断,不能用mid判断
for (int j = i - 1; j >= left; j--) {
array[j + 1] = array[j];
}
array[left] = cur;
//
}
}
3.冒泡排序
第一趟:i=0,需要比较n-1次
第二趟:i=1,需要比较n-2次
…
第n-1趟,i=n-2,需要比较1次
那么共需要比较1+2+3+…+n-1=n(n-1)/2
平均复杂度为O(n^2)
public static void bubbleSort(int[] array){
/*时间O(n^2),空间O(1),原地排序,稳定排序
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
}
//只需要循环n-1次,每次把无序区间的最大元素冒泡到无序区间的最右边
//i=0第1次循环,无序区间[0,n-1],有序区间[];
//i=1第2次循环,无序区间[0,n-2],有序区间[n-1,n-1];
for (int i = 0; i < array.length - 1; i++) {
//每轮冒泡不需要和有序区间的元素做多余的比较了
for (int j = 0; j < array.length - i - 1; j++) {
if(array[j] > array[j + 1]){
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
优化1:
假设某一趟冒泡无元素发生交换,那么表示已经满足有序了,就不需要再进行之后的趟数比较了
代码如下:
public static void bubbleSort(int[] array){
/*时间O(n^2),空间O(1),原地排序,稳定排序
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
}
//只需要循环n-1次,每次把无序区间的最大元素冒泡到无序区间的最右边
//i=0第1次循环,无序区间[0,n-1],有序区间[];
//i=1第2次循环,无序区间[0,n-2],有序区间[n-1];
for (int i = 0; i < array.length - 1; i++) {
//每轮冒泡不需要和有序区间的元素做多余的比较了
boolean exchange = false;
for (int j = 0; j < array.length - i - 1; j++) {
if(array[j] > array[j + 1]){
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
exchange = true;//存在位置交换即置为true
}
}
//一趟下来是否发生了位置交换
if(!exchange){
return;
}
}
}
分析复杂度:
1)初始状态若为正序,一次扫描即可完成排序,比较次数为n-1,移动次数为0,时间复杂度O(n)
2)初始状态若为逆序,需要n-1次排序,每次n-i-1次比较(0 <= i <= n-2)
比较次数为1+…+n-1=n(n-1)/2
最大移动次数为3*n(n-1)/2
时间复杂度O(n^2)
2)平均情况下,O(n^2)
4.希尔排序
注:基于插入排序的思想
插入排序对于小规模数据数据或者基本有序数据十分高效,但是如果规模较大或者基本无序性能就会很低,而希尔排序可以解决这个问题
public static void shellSort(int[] array){
/*希尔排序的复杂度和增量序列是相关的
空间复杂度O(1),原地排序,非稳定排序
* */
if(array == null || array.length == 0){
System.out.println("输入数组非法");
return;
}
int n = array.length;
//初始增量是数组长度的一半
int gap = n / 2;
int cur,before;
while(gap > 0){
//对各个组进行插入的时候并不是先对一个组进行排序完再对另一个组进行排序,
// 而是随着i++,会轮流对每个组进行插入排序
//下标0-gap-1是每个分组的第一个元素,不需要排序
for (int i = gap; i < n; i++) {
cur = array[i];
//一直向前寻找直到找到一个<=cur的元素
for (before = i - gap;before >= 0 && array[before] > cur;before -= gap){
array[before + gap] = array[before];//不符合就顺序后移
}
//如果找到before位置元素<=cur,那么before+gap位置就是最终要找的位置
array[before + gap] = cur;
}
gap /= 2;
}
}
5.归并排序
归并排序就是将待排序的数分成两半后排好序,然后再将两个排好序的序列合并成一个有序序列。
分:把原数组分为两个子数组的归并排序
治:在切割后的子数组只要满足left < right的时候就一直分;
合:合并切割后的子数组为有序数组,依次向上合并
A.递归写法
使用递归实现自顶向下的归并排序
public static void mergeSort(int[] array,int left,int right){
/*
时间复杂度O(nlogn),空间复杂度O(n),
稳定排序(因为在合并的时候,如果相等,选择前面的元素到辅助数组),
非原地排序
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return;
}
//当left==right表示只有一个元素需要归并排序,那么直接返回即可
if(left >= right){
return;
}
//注意这里是if而不是while,while会一直死循环
if (left < right){
//防止两个很大的数相加溢出变为负数
int middle = left + ((right - left) >> 1);
//对左半部分进行归并排序
mergeSort(array,left,middle);
//对右半部分进行归并排序
mergeSort(array,middle+1,right);
//对左半部分和右半部分进行合并
merge(array,left,middle,right);
}
}
private static void merge(int[] array, int left, int middle, int right) {
/*合并函数,将两个有序的数组合并为一个数组*/
//左部分数组[left,middle],右部分数组[mid,right]
int[] tempArray = new int[right - left + 1];
int tempIndex = 0;
int p1 = left;//遍历数组1的指针
int p2 = middle + 1;//遍历数组2的指针
while (p1 <= middle && p2 <= right){
if(array[p1] <= array[p2]){
tempArray[tempIndex++] = array[p1++];
}else{
tempArray[tempIndex++] = array[p2++];
}
}
//不满足p2 <= right即右部分数组元素遍历完成
while (p1 <= middle){
tempArray[tempIndex++] = array[p1++];
}
//不满足p1 <= middle即左部分数组元素遍历完成
while(p2 <= right){
tempArray[tempIndex++] = array[p2++];
}
//此时tempIndex指向临时数组最后一个位置的下一个位置,也就是总共有tempIndex个元素
//将辅助数组的值复制回原数组
//在合并过程中left,mid,third均有变化,所以这里以initial和right作为区间锁定
for (int i = 0; i < tempIndex; i++) {
array[i + left] = tempArray[i];
}
}
归并排序的优化:
1)在合并数组过程中,由底向上合并的,如果底层的两个子数组是有序状态,那么这次就可以不进行多余的合并操作,直接保持原序即可
if (left < right){
//防止两个很大的数相加溢出变为负数
int middle = left + ((right - left) >> 1);
//对左半部分进行归并排序
mergeSort(array,left,middle);
//对右半部分进行归并排序
mergeSort(array,middle+1,right);
//由底向上的归并过程本身已经保证了l-middle,middle+1-right是有序的
//所以只有array[middle] > array[middle + 1]才需要merge操作
if(array[middle] > array[middle + 1]){
merge(array,left,middle,right);
}
}
2)1)基础上还可以优化,由于插入排序在规模较小的数组优势明显,所以当待排序数组长度不大于15时可以考虑插入排序
public static void mergeSort(int[] array,int left,int right){
/*优化2,待排序数组长度足够小时可以考虑插入排序
时间复杂度O(nlogn),空间复杂度O(n),
稳定排序(因为在合并的时候,如果相等,选择前面的元素到辅助数组),
非原地排序
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return;
}
//当left==right表示只有一个元素需要归并排序,那么直接返回即可
if(right - left <= 15){
insertionSort(array,left,right);
return;
}
//注意这里是if而不是while,while会一直死循环
if (left < right){
//防止两个很大的数相加溢出变为负数
int middle = left + ((right - left) >> 1);
//对左半部分进行归并排序
mergeSort(array,left,middle);
//对右半部分进行归并排序
mergeSort(array,middle+1,right);
//由底向上的归并过程本身已经保证了l-middle,middle+1-right是有序的
//所以只有array[middle] > array[middle + 1]才需要merge操作
if(array[middle] > array[middle + 1]){
merge(array,left,middle,right);
}
}
}
private static void insertionSort(int[] array, int left, int right) {
/*对array数组的left-right区间的元素进行插入排序*/
int cur,j;
for (int i = left + 1; i <= right; i++) {
cur = array[i];
for (j = i - 1; j >= left && array[j] > cur ; j--) {
array[j + 1] = array[j];
}
array[j + 1] = cur;
}
}
private static void merge(int[] array, int left, int middle, int right) {
......
}
B.非递归写法
即自底向上的归并排序:
我们可以使用迭代来代替递归
由于merge操作时间复杂度仍为O(nlogn),空间复杂度O(n),
稳定排序(因为在合并的时候,如果相等,选择前面的元素到辅助数组),
非原地排序
版本1:
public static void mergeSort(int[] arr){
int n = arr.length;
//每次要merge的子数组元素个数
int mergeCount;
//mergeCount从1,2,4,8....mergeCount范围<=n,而不是和n/2 + 1
for (mergeCount = 1; mergeCount <= n ; mergeCount += mergeCount) {
for (int i = 0; i < n; i += 2 * mergeCount) {
//第一轮merger的两个子数组是[0,mergeCount-1],[mergeCount,2*mergeCount-1]
//第二轮:[2*mergeCount,3*mergeCount-1],[3*mergeCount,4*mergeCount-1]
//i < n,但是i + mergeCount - 1和i + 2 * mergeCount - 1有可能越界
int middle = Math.min(i + mergeCount - 1,n - 1);
int right = Math.min(i + 2 * mergeCount - 1,n - 1);
merge(arr,i,middle,right);
}
}
}
版本2:
public static void mergeSort(int[] arr) {
/*归并排序的非递归写法
时间复杂度O(nlogn),空间复杂度O(n),
稳定排序(因为在合并的时候,如果相等,选择前面的元素到辅助数组),
非原地排序
* */
int n = arr.length;
// 子数组的大小分别为1,2,4,8...
// 刚开始合并的数组大小是1,接着是2,接着4....
//i为每次merge的元素个数
for (int i = 1; i < n; i += i) {
//进行数组进行划分
int left = 0;
int mid = left + i - 1;
int right = mid + i;
//进行合并,对数组大小为 i 的数组进行两两合并
while (right < n) {// 合并函数和递归式的合并函数一样
// 合并函数和递归式的合并函数一样
merge(arr,left,mid,right);
left = right + 1;
mid = left + i - 1;
right = mid + i;
}
// 还有一些被遗漏的数组没合并,千万别忘了
// 因为不可能每个子数组的大小都刚好为 i
if (left < n && mid < n) {
merge(arr, left, mid, n - 1);
}
}
}
6.快速排序
参考文章:
https://mp.weixin.qq.com/s/izV79GJI3xZef03axneerw
1.单路快速排序法
public static void quickSort(int[] array){
/*快速排序--单路快速排序法
平均时间复杂度O(nlogn),最坏O(n^2),空间复杂度O(logn)
原地排序,不稳定排序(中轴元素与arr[j]发生交换的时候,可能破坏稳定性)
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return;
}
__quickSort(array,0,array.length - 1);
}
private static void __quickSort(int[] array, int low, int high) {
/*对array[low,high]部分进行快速排序
* */
if (low < high){
int partition = partition(array,low,high);
__quickSort(array,low,partition - 1);
__quickSort(array,partition + 1,high);
}
}
private static int partition(int[] array, int low, int high) {
int base = array[low];
//初始array[l+1,j]<base,array[j+1,i)>base
int j = low;
//此定义使得上面两个区间初始化都是空
for (int i = low + 1; i <= high; i++) {
if(array[i] < base){
int temp = array[i];
array[i] = array[j + 1];
array[j + 1] = temp;
j++;//[l+1,j]扩充了一个元素
}
}
//当遍历完成的时候j所指向的位置就是>base区间的第一个元素
int temp = array[j];
array[j] = base;
array[low] = temp;
return j;
}
当快速排序有序,即完全正序或者完全倒序时,是最差的情况,此时整颗递归树的高度是n,此时时间复杂度是O(n^2)
当每次选到一个中轴元素刚好位于中间,是最好的情况,此时整颗递归树高度是logn,此时时间复杂度是O(nlogn)
优化1:
为了降低快速排序退化为O(n^2)的概率,我们应该尽可能的选取数组中间的元素作为基准元素,这样期望的递归树高度是logn
将partition方法修改代码如下:
private static int partition(int[] array, int low, int high) {
/*随机选取一个元素作为基准元素*/
Random random = new Random();
int randomIndex = random.nextInt(high - low + 1) + low;
int extra = array[low];
array[low] = array[randomIndex];
array[randomIndex] = extra;
int base = array[low];
//初始array[l+1,j]<base,array[j+1,i)>base
int j = low;
//此定义使得上面两个区间初始化都是空
for (int i = low + 1; i <= high; i++) {
if(array[i] < base){
int temp = array[i];
array[i] = array[j + 1];
array[j + 1] = temp;
j++;//[l+1,j]扩充了一个元素
}
}
//当遍历完成的时候j所指向的位置就是>base区间的第一个元素
int temp = array[j];
array[j] = base;
array[low] = temp;
return j;
}
优化2:
对于高级排序在数据规模较小时,可用插入排序实现
private static void __quickSort(int[] array, int low, int high) {
/*对array[low,high]部分进行快速排序
* */
if(high - low < 15){
insertionSort(array,low,high);
return;
}
if (low < high){
int partition = partition(array,low,high);
__quickSort(array,low,partition - 1);
__quickSort(array,partition + 1,high);
}
}
private static void insertionSort(int[] array, int left, int right) {
/*对array数组的left-right区间的元素进行插入排序*/
int cur,j;
for (int i = left + 1; i <= right; i++) {
cur = array[i];
for (j = i - 1; j >= left && array[j] > cur ; j--) {
array[j + 1] = array[j];
}
array[j + 1] = cur;
}
}
2.双路快速排序法
1中的单路快速排序当有大量重复值的时候递归树会极不平衡,退化为O(n^2)的概率很大
我们可以优化partition方法
考虑采用两个指针i,j,分别向后,向前遍历,使得[l+1,i) <= v,(j,r] >= v(把==v的元素分布到左右两部分)
public static void quickSort(int[] array){
/*快速排序--单路快速排序法
平均时间复杂度O(nlogn),最坏O(n^2),空间复杂度O(logn)
原地排序,不稳定排序(中轴元素与arr[j]发生交换的时候,可能破坏稳定性)
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return;
}
__quickSort(array,0,array.length - 1);
}
private static void __quickSort(int[] array, int low, int high) {
/*对array[low,high]部分进行快速排序
* */
if(high - low < 3){
insertionSort(array,low,high);
return;
}
if (low < high){
int partition = partition(array,low,high);
__quickSort(array,low,partition - 1);
__quickSort(array,partition + 1,high);
}
}
private static void insertionSort(int[] array, int left, int right) {
/*对array数组的left-right区间的元素进行插入排序*/
int cur,j;
for (int i = left + 1; i <= right; i++) {
cur = array[i];
for (j = i - 1; j >= left && array[j] > cur ; j--) {
array[j + 1] = array[j];
}
array[j + 1] = cur;
}
}
private static int partition(int[] array, int low, int high) {
/*随机选取一个元素作为基准元素*/
Random random = new Random();
int randomIndex = random.nextInt(high - low + 1) + low;
int extra = array[low];
array[low] = array[randomIndex];
array[randomIndex] = extra;
//获取基准元素
int base = array[low];
//初始array[low+1,i) <= base,array(j,high] >= base
int i = low + 1,j = high;//初始值使得上面两个空间是空
while (true){
//注意越界问题,i有最右边界,j有最左边界
while (i <= high && array[i] < base){
i++;
}
while (j >= low + 1 && array[j] > base){
j--;
}
//判断越界要在交换前判断,当i==j的时候满足这个元素>=base,也满足<=base
// 那么一定是等于base的元素,故只需要break即可
if(i >= j){
break;
}
int temp = array[i];
array[i] = array[j];
array[j] = temp;
i++;
j--;
}
//跳出while循环时即越界的时候,i停在从前向后看第一个>=v的元素,
//j停在从后向前看的第一个<=v的元素
//基准元素和j位置元素交换
int temp = array[j];
array[j] = base;
array[low] = temp;
return j;
}
对于近乎有序数组和大量重复数组尤其是大量重复数组,双路快速排序性能提升明显
3.三路快速排序法
2中对于==V元素的处理还可以更优化,我们可以用三个指针把数组分为 <v, =v , >v 的三部分,对于 =v 的部分直接忽略即可,只需要递归的对 <v , >v 的部分进行快速排序
array[l+1,rt] < v
array[lt + 1,i - 1] == v
array[gt,r] > v
i指针负责遍历,lt,gt指针负责处理<v , >v区间的扩容
public static void quickSort(int[] array){
/*快速排序--单路快速排序法
平均时间复杂度O(nlogn),最坏O(n^2),空间复杂度O(logn)
原地排序,不稳定排序(中轴元素与arr[j]发生交换的时候,可能破坏稳定性)
* */
if(array == null || array.length == 0){
System.out.println("传入数组非法");
return;
}
__quickSort(array,0,array.length - 1);
}
private static void __quickSort(int[] array, int low, int high) {
/*对array[low,high]部分进行三路快速排序
* */
if(high - low < 3){
insertionSort(array,low,high);
return;
}
if (low < high){
//随机选取一个元素作为基准元素
Random random = new Random();
int randomIndex = random.nextInt(high - low + 1) + low;
int extra = array[low];
array[low] = array[randomIndex];
array[randomIndex] = extra;
int base = array[low];
/*初始array[low+1,lt] < base,
array[lt + 1,i - 1] == base,
array[gt,high] > base*/
int lt = low,gt = high + 1,i = low + 1;//初始值使得上面三个区间是空
while(true){
if(array[i] > base){
int temp = array[i];
array[i] = array[gt - 1];
array[gt - 1] = temp;
//交换后i指向的依然是一个未遍历的元素所以i不更新
gt--;//>v的元素区间扩容
}else if(array[i] < base){
int temp = array[lt + 1];
array[lt + 1] = array[i];
array[i] = temp;
lt++;//<v的元素区间扩容
i++;//交换后i位置元素==v,所以i++判断下一个元素
}else{
i++;//如果==v那么继续向下遍历即可
}
//说明全部元素遍历完成
if(i >= gt){
break;
}
}
//交换,最后只需要把基准元素和<base区间的最后一个元素交换
array[low] = array[lt];
array[lt] = base;
//交换后lt--1
lt--;
//只需要对<base,>base的区间进行递归
__quickSort(array,low,lt);
__quickSort(array,gt,high);
}
}
三路快速排序法对于处理存在大量重复值时性能远优于一路,二路快速排序,但是对于随机和近乎有序的数组,没有二路快速排序快
7.堆排序
参考文章:
https://mp.weixin.qq.com/s?__biz=MzU1MDE4MzUxNA==&mid=2247483788&idx=1&sn=23bd1c47f0d3fe97340f2fea957dfc78&chksm=fba536b9ccd2bfafdbc4d70f80fa89cdc1edeffda1fc1fbafc5e1786faad5206233c8bc8a4fe&scene=21#wechat_redirect
二叉堆的概念:
如果对于每个父节点的值都大于等于(或者小于等于)其两个孩子的值,那么就称这种特殊的数据结构为堆(这个条件也被称作堆有序性)
(所谓完全二叉树,简单来说,就是除了最后一层外每一层节点数均达到最大值,并且最后一层的节点都连续集中在最左边)
堆总是一棵完全二叉树
我们可以使用数组来存储二叉堆:
根节点标记为0,从上到下每层按从左到右的顺序标记每个节点,将元素存入对应下标的数组中。那么左孩子索引是2 * i + 1,右孩子索引是2 *i + 2;
且i位置的元素对应的父亲索引是(i - 1) / 2;
最后一个非叶子节点索引(count - 1) / 2;
public static void heapSort(int[] array){
/*原地堆排序,整个数组的排序过程在原地进行,不需要任何额外空间。
时间复杂度:O(nlogn),空间复杂度:O(1),非稳定排序,原地排序
* */
//1.给定一个数组,使之成为堆(大顶堆)
int n = array.length;
//最后一个非叶子节点索引:(n - 1) / 2,叶子节点本身已是最大堆
for (int i = (n - 1) / 2; i >= 0; i--) {
//对第i个位置的元素进行下沉操作
shiftDown(array,n,i);
}
//2.完成堆排序:堆顶元素和当前堆的最后一个元素交换,每交换后对新的堆顶元素进行下沉操作
//使之再次成为最大堆,再交换,如此下去直到整个数组排序完成
for (int i = n - 1;i > 0;i--){
//取堆顶元素(当前的最大元素)与最后一个元素交换
int temp = array[0];
array[0] = array[i];
array[i] = temp;
//交换后不再是一个最大堆,最大堆剩余n-1个元素,对交换后的堆顶元素进行下沉操作使其成为新的最大堆
//交换后把最后一个元素从数组中逻辑删除,还剩n-1个元素(也可以用i表示)
shiftDown(array,i,0);
}
}
private static void shiftDown(int[] array,int length,int cur) {
/*在array的n个元素所定义的堆中,尝试下沉索引为cur的元素为其找到其合适的位置
* */
int maxChildIndex;
while (2 * cur + 1 < length){//存在左孩子(2 * cur + 1不越界)
maxChildIndex = 2 * cur + 1;//定位左孩子节点的位置
// 如果存在右孩子且右孩子节点比左孩子大,则定位到右孩子
if(maxChildIndex + 1 < length && array[maxChildIndex + 1] > array[maxChildIndex]){
maxChildIndex += 1;
}
//如果当前节点大于其最大孩子节点则下沉结束,这就是其最终位置
if(array[cur] >= array[maxChildIndex]){
break;
}
//当前节点与最大孩子节点交换位置,如果新的位置还存在孩子则继续执行下沉操作
int temp = array[cur];
array[cur] = array[maxChildIndex];
array[maxChildIndex] = temp;
cur = maxChildIndex;
}
}
上述排序算法参考文章:
https://mp.weixin.qq.com/s/4AvOsuwQlcut9DSZjiSTfw
https://mp.weixin.qq.com/s/IAZnN00i65Ad3BicZy5kzQ?utm_source=wechat_session&utm_medium=social&utm_oi=749010603246383104