文章目录
排序
前言
稳定性
一组待排序的数据中,如果存在相同的数值,排序后多个相同数值的相对位置不变,就是稳定的;否则不稳定
排序分类
内部排序:数据都在内存中进行操作的,大多数排序都是这种
外部排序:数据很大,内存装不下,这时候就需要运用外部排序了
7大比较排序
插入排序
类似于:我们平时玩的扑克牌,在我们拿牌的时候,都是将小的往前面进行插入(这就是排升序)
动图展示
单趟:有一个有序数组,现在新来一个值,需要将这个值放入该数组,并使这个数组仍然有序。这时我们就可以将这个元素与数组中的元素从后往前进行比较,找到自己合适的位置,然后进行插入
复合:在自己不知道有几个有序的时候,这时候我们就可以把第一个数当作有序的,然后从第二个数开始,依次进行单趟排序
代码
public static void insertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int end = i-1; // 当while循环结束的时候,end+1就是要待排序元素的位置了
int tmp = array[i]; // 待排序元素
while (end >= 0) {
if(array[end] > tmp) {
array[end+1] = array[end];
end--;
} else {
break;
}
}
// 两种情况
// 1. end = -1
// 2. array[end] <= tmp
array[end + 1] = tmp;
}
}
总结
- 插入排序是一个稳定排序
- 时间复杂度:
- 最好:本来数组处于顺序情况(O(N))
- 最坏:每个元素都需要挪动 i 次(O(n*(1+2+3+……+ n-1))----> O(N^2))
- 空间复杂度:
- O(1)
- 使用场景:数据量少且趋近于有序的情况下,直接插入排序的时间复杂度趋近于 O(N)
希尔排序
针对插入排序的升级,运用分组的思想(先每一组都趋近于有序,然后慢慢变成整体趋近于有序了)
先选定一个整数gap,把待排序文件中所有记录分成gap个组,所有距离为gap的记录分在同一组内,并对每一组内的元素进行插入排序。然后将gap逐渐减小重复上述分组和排序的工作。当gap=1时,就直接对整个数组进行插入排序了**【这时排序的数组已经趋近于有序了,就很适合用插入排序了】**。
画图展示
动图
代码
public static void shellSort(int[] array) {
int gap = array.length;
while (gap > 1) {
gap /= 2;
shellSortHelper(array,gap);
}
shellSortHelper(array,1);
}
private static void shellSortHelper(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int end = i-gap;
int tmp = array[i]; // 待排序元素
while (end >= 0) {
if(array[end] > tmp) {
array[end+gap] = array[end];
end-=gap;
} else {
break;
}
}
array[end + gap] = tmp;
}
}
总结
- 希尔排序是一个不稳定排序
- 时间复杂度:希尔排序时间复杂度不要计算,预估:O(N^1.3 ~ N^1.5)
- 空间复杂度:O(1)
- 当 gap > 1 时都是预排序,gap = 1 的时候,才是真正的排序
选择排序
每趟找一个最小的值放待排序序列的最前面
升级版:一趟遍历在待排序区间 找一个最小的 + 一个最大的
动图展示
代码
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int tmp = i;
for (int j = i; j < array.length; j++) {
if(array[j] < array[tmp]) {
tmp = j;
}
}
// 交换 tmp,i的值
swap(array,tmp,i);
}
}
改进
注意:当交换最小值后,在交换最大值(这就需要考虑先交换的最小值时会不会把你需要的最大值给换走了)
public static void selectSort1(int[] array) {
int left = 0, right = array.length-1;
while (left < right) {
int minIndex = left;
int maxIndex = right;
for (int i = left; i <= right; i++) {
if(array[i] < array[minIndex]) {
minIndex = i;
}
if(array[i] > array[maxIndex]) {
maxIndex = i;
}
}
// 交换
// 注意
swap(array,minIndex,left);
if(maxIndex == left) {
maxIndex = minIndex;
}
swap(array,maxIndex,right);
right--;
left++;
}
}
总结
- 选择排序是不稳定的
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 使用改进版本,可以提高一半时间复杂度
堆排序
堆介绍
堆是一颗完全二叉树,存储方式是拿数组存储的【不存在浪费空间】,根据完全二叉树的层序遍历来标记下标
小根堆:根节点的值 < 孩子节点的值
大根堆:根节点的值 > 孩子节点的值
二叉树的结论:
- 已知父亲节点下标 i ,左孩子下标 2*i+1 右孩子下标 2 * i+2
- 已知孩子下标 i ,父亲节点下标 (i - 1)/ 2
建堆调整过程【自顶向下】
都是从最后一棵子树开始调整,向下调整
最后一棵子树的父节点下标:p = (array.length-1-1) / 2,(最后一个元素的下标 - 1)/ 2;调整完后 p-- ,直到调整完 0 下标的这棵树,每次调整的结束条件:1. 此树符合条件了;2. 访问的下标 < array.length
向下调整
public void creatHeap(int[] array) {
for (int p = (array.length-1-1)/2; p >= 0; p--) {
shiftDown(array,p,array.length);
}
}
private void shiftDown(int[] array, int root, int len) {
int parent = root;
int child = 2*parent+1; // 左孩子
while (child < len) {
// 至少有一个孩子,大根堆:找到两个孩子中的较大值,再与 parent比较 > parent就换,否则不交换
if(child + 1 < len && array[child] < array[child+1]) {
child = child+1;
}
// 判断array[child] 与 array[parent]的大小
if(array[child] > array[parent]) {
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
parent = child;
child = 2*parent+1;
} else {
// 此树满足条件,不需要调整了
break;
}
}
}
自底向上建堆【shiftDown】的复杂度:O(N)
向上调整
private void shiftUp(int child) {
int parent = (child-1)/2;
while (child > 0) {
if(elem[parent] < elem[child]) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
child = parent;
parent = (child-1)/2;
} else {
break;
}
}
}
向上调整建堆【每次push,然后调用shiftUp】的时间复杂度
因为每次都要向下调整,而有 n 个数需要调整,所以复杂度为:N * logN(需要调整个数 * 树的高度)
堆排序
排序前需要建堆
从小到大排序:建大堆【每次将堆顶元素放最后,然后调整 0 这棵树,就相当于最大的数排好序了】
从大到小排序:建小堆【每次将堆顶元素放最后,然后调整 0 这棵树,就相当于最小的数排好序了】
public void HeapSort() {
// 从小到大----建立大堆
// 每次取堆顶放到最后【最大值放好位置了】,然后调整0~end-1
int end = usedSize-1;
while (end > 0) {
int tmp = elem[0];
elem[0] = elem[end];
elem[end] = tmp;
end--;
shiftDown(0,end);
}
}
总结
- 时间复杂度:N * logN 个数 * 树的高度
- 空间复杂度:
- 稳定性:不稳定
冒泡排序
外层循环:需要排的趟数(数组长度-1),每一趟下来,就排好一个数
内层循环:每一趟排序需要比较的对数(两两比较的对数)
动图展示
代码
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length-1; i++) {
boolean flag = true;
for (int j = 0; j < array.length-i-1; j++) {
if(array[j] > array[j+1]) {
swap(array,j,j+1);
flag = false;
}
}
if(flag) {
break;
}
}
}
总结
- 冒泡排序是稳定的
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
快速排序
基本思想:每次遍历,都找一个 pivot 基准值,这个基准值的左边都比它小,右边都比它大,然后递归右边找基准,递归左边找基准**【相当于每次遍历,排好一个数】**,【类似于二叉树,我们找根节点,然后分成左子树 和 右子树】
Hoare版本
将当前序列的最左边的数作为基准,然后右边找一个小于基准的,左边找一个大于基准的【注意左边做key,右边先找值;右边做key时,左线先找值】,然后两个交换;继续遍历找下一组,直到 left < right 就结束了,结束后,将 left下标 与 基准元素的下标交换
画图展示
代码
private static void quick(int[] array, int low, int high) {
// = 表示:当前序列只有一个元素了,就不需要递归了;> 表示:类似于左子树递归完了,递归右子树去了[右子树可能为 null]
if(low >= high) return;
int pivot = partition(array,low,high);
quick(array,low,pivot-1);
quick(array,pivot+1,high);
}
private static int partition(int[] array, int left, int right) {
int key = array[left];
int tmp = left;
while (left < right) {
// 注意这两个的while循环中的 array[] >= key 中的等于号,
// 若没有等于号的情况下,array[left] == array[right]:内部两个while循环都进不去,外层死循环了
while (left < right && array[right] >= key) {
right--;
}
while(left < right && array[left] <= key) {
left++;
}
swap(array,left,right);
}
swap(array,left,tmp);
return left;
}
挖坑法
首先记录 key = array[left] 相当于把 当前序列的 left 作为坑,右边找一个大于 key 的放 left 坑里,右边的就成坑了;然后左边找一个小于 key 的,放 right 坑里;以此类推,然后 left < right时,将 key 放left这个坑里
画图展示
代码
pJavrivate static int Hole(int[] array, int left, int right) {
int key = array[left];
while (left < right) {
// 注意这两个的while循环中的 array[] >= key 中的等于号,
// 若没有等于号的情况下,array[left] == array[right]:内部两个while循环都进不去,外层死循环了
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;
}
前后指针法【了解】
思路:i 负责找一个小于 tmp 的值,d 负责保存 大于 tmp 的下标,最后 i d 交换
public static int partition1(int[] array, int left, int right) {
int d = left + 1;
int tmp = array[left];
for(int i = left + 1; i<=right; i++) {
if(array[i] < tmp) {
swap(array,i,d);
d++;
}
}
swap(array,d-1,left);
return d-1;
}
三数取中法【优化】
left,right,mid 中取第二大的值的下标作为最终的哨兵key,然后用partition函数找 pivot,这就使每次划分序列都较为均匀,有效的减少了递归深度
当数组趋近于有序的时候,可以采用插入排序进行优化
public static void insertSortRange(int[] array, int left, int right) {
for (int i = left; i <= right; i++) {
int end = i - 1; // 当while循环结束的时候,end+1就是要待排序元素的位置了
int tmp = array[i]; // 待排序元素
while (end >= left) {
if (array[end] > tmp) {
array[end + 1] = array[end];
end--;
} else {
break;
}
}
array[end + 1] = tmp;
}
}
private static int medianOfThreeIndex(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;
}
}
}
private static void quick(int[] array, int low, int high) {
if(low >= high) return;
if(high - low < 40) {
insertSortRange(array,low,high);
}
// 减少递归深度
int index = medianOfThreeIndex(array,low,high);
swap(array,low,index);
int pivot = partition1(array,low,high);
quick(array,low,pivot-1);
quick(array,pivot+1,high);
}
非递归版本
借助栈,还是建立在找基准的基础上,找到 pivot 时,会划分成两个待排序序列,然后将两个序列的区间下标放入栈中,每次从栈中弹出两个值,然后针对这个区间找基准,最后栈为空,就排好序了
public static void norQuick(int[] array) {
Stack<Integer> stack = new Stack<>();
int left = 0, right = array.length-1;
int pivot = Hole(array,left,right);
// 表示[left,pivot]至少有两个元素
if(pivot > left+1) {
stack.push(left);
stack.push(pivot-1);
}
if(pivot < right-1) {
stack.push(pivot+1);
stack.push(right);
}
while (!stack.isEmpty()) {
right = stack.pop();
left = stack.pop();
pivot = Hole(array,left,right);
// 表示[left,pivot]至少有两个元素
if(pivot > left+1) {
stack.push(left);
stack.push(pivot-1);
}
if(pivot < right-1) {
stack.push(pivot+1);
stack.push(right);
}
}
}
总结
- 时间复杂度:
- 最好:每次 pivot 都是在当前序列的最中间,将当前序列完美分成对此的两部分,每层 n 个数,完全二叉树高度:logN,最好就是 N*logN
- 最坏:单分支树 O(N^2)
- 空间复杂度:
- 最好:完全二叉树的高度:O(logN)
- 最坏:单分支树的高度(数组的长度) O(N)
- 稳定性:不稳定
还可以看看这篇文章:快排优化
归并排序
也是采用分治的思想,先将数组拆分成一个一个的元素,然后两个两个合并,两个合并结束;四个四个合并……一直到完全排好序(若是将两个有序集合合并,称为二路归并)
图解
递归版本
public static void mergeSort(int[] array) {
mergeSortIntenal(array,0,array.length-1);
}
private static void mergeSortIntenal(int[] array, int low, int high) {
if(low >= high) {
return;
}
int mid = (low+high)/2;
mergeSortIntenal(array,low,mid);
mergeSortIntenal(array,mid+1,high);
merge(array,low,mid,high);
}
private static void merge(int[] array, int low, int mid, int high) {
// 1. 合并[low,mid],[mid+1,high] 创建数组:high-low+1
int s1 = low;
int e1 = mid;
int s2 = mid+1;
int e2 = high;
int[] tmpArr = new int[high-low+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++];
}
// 将tmpArr中的数据放入array
System.arraycopy(tmpArr,0,array,low,k);
}
非递归版本
public static void mergeSort1(int[] array) {
int gap = 1;
while (gap < array.length) {
// for 循环控制2个2个排序后合并;4个4个排序后合并……
for (int i = 0; i < array.length; i+=2*gap) {
int left = i;
int mid = left + gap - 1;
if(mid >= array.length) {
mid = array.length-1;
}
int right = mid + gap;
if(right >= array.length) {
right = array.length-1;
}
merge(array,left,mid,right);
}
gap *= 2;
}
}
总结
- 时间复杂度:N * log(N)
- 空间复杂度:O(N)
- 稳定性:稳定
3大非比较排序
计数排序
需要创建一个数组,数组大小(最大值 - 最小值 + 1), 然后将数组中的值的个数放入对应的下标中
public static void func(int[] array) {
// 1. 找最大最小值
int max = 0, min = Integer.MAX_VALUE;
for (int i = 0; i < array.length; i++) {
if(array[i] > max) {
max = array[i];
}
if(array[i] > min) {
min = array[i];
}
}
// 2. 创建数组
int[] arr = new int[max-max+1];
Arrays.fill(arr,min-1); // 填充一个数组中没有的元素
for (int i = 0; i < array.length; i++) {
arr[array[i]-min]++;
}
// 将元素填充进array
int k = 0;
for (int i = 0; i < arr.length; i++) {
if(arr[i] > 0) {
for (int j = 0; j < arr[i]; j++) {
array[k++] = arr[i]+min;
}
}
}
}
基数排序
根据每位数进行排序的(个位,十位,百位……),排到最高位的时候就是有序了,所以需要 存放每位上的数字的10个队列(存放 0 - 9)
桶排序
这个也需要队列:感觉就是 基数排序 + 计数排序
桶的数量:最大值 - 最小值 + 1,然后桶里面放的也是每个元素的个数,最后遍历拿值就可以了