🍉作者@ Autumn60
🏄欢迎关注:👍点赞🙌收藏✍️留言
目录
排序:
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作
稳定性:
一个数组中具有相同的两个元素加入是5a(在前)和5b,如果在经过排序后,5a在5b的前面,说明这个排序是稳定的,反:不稳定的排序;
稳定性的意义:
如果A,20分作做完卷子得满分,B,30分钟做完卷子得了满分,但是排名的时候B却排到了A的前面,则是不公平的
图例:
内部排序:数据元素全部放在内存中的排序
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
七大排序:
如果一个排序是稳定的,那么可以实现为不稳定的; 如果一个排序是不稳定的,那么没有能力实现为稳定的
一、插入排序:
思路:将待排序的元素按照自身大小逐个插入到已经排好序的有序序列中,直到所有的元素插完为止。
目标:从小到大排序
代码:
public void insertSort(int[] array) {
//拿到手之后直接插入排序
for (int i = 1; i < array.length; i++) {
//假设i下标是最小值,和i后边的每一位元素互相比较,直到i走到最后一位就比较完毕了
int tmp = array[i];
for (int j = i-1; j >= 0; j--) {
if(tmp < array[j]) {
array[j+1] = array[j];
array[j] = tmp;
} else {
break;
}
}
}
}
特点:
时间复杂度:O(n^2) 最优时间复杂度:O(n) 优点:当数据趋于有序的时候,排序速度回非常快 使用场景:数据基本有序,建议使用直接插入排序 稳定性 : 稳定;
二、希尔排序
思想:
先选定一个整数,把待排序文件中所有记录分成多个组, 所有距离为一样的分成一个组,并对每一组内的记录进行排序。重复上述分组和排序的工作。当组数到达 =1 时,所有记录在统一组内排好序 。也就是分组之后,对每一组进行插入排序,尽量把最大和最小的往两边推,一开始组数多,数据小,后面组数少,数据多,但是数据越趋于有序,会越排序越快
代码:
public void shellsort(int[] array) {
//gap就是要分的组数
int gap = array.length;
while(gap > 1) {
gap /= 2 ;
Shell(array,gap);
}
}
private void Shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = i-gap;
for (; j >= 0; j-=gap) {
if(array[j] > tmp){
array[j+gap] = array[j];
} else {
break;
}
}
array[j+gap] = tmp;
}
}
特点:
1.希尔排序是对直接插入排序的优化
2.在组数不等于1之前,都是预排,就是把最大值和最小值往两边推,最后为1组的时候再进行排序,所以希尔排序会越排速度越快;
稳定性:不稳定
时间复杂度:因为组数很难确定,在很多书中给出的时间复杂度都不固定。大约是在n1.25 到1.6*n1.25 范围内;
三、 选择排序
思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
解法 一:双层循环,里层循环找未排序的最小值,外层循环控制排序好的数字;
代码:
当里层循环走完,minIndex里的一定是未排序数字,最小值的下标,找到交换即可;
public void selectSort(int[] array) {
for (int j = 0; j < array.length; j++) {
int minIndex = j;
for (int i = j+1; i < array.length; i++) {
if(array[minIndex] > array[i]) {
minIndex = i;
}
}
//这里的swap方法是交换方法
swap(array,j,minIndex);
}
}
解法二:找最小值和最大值 ,两个交换;
题解过程:
再定义一个最小值和最大值,假设Left里面存储的就是最大值和最小值,进入循环i,让i从Left + 1 的位置开始循环,小于 Right , 每次进来都要比较,如果有 i 下标的 值比 minIndex(最小值)小,就更新minIndex的下标, 如果 i 下标的值 比 MaxIndex(最大值)下标的值大,就更新最大值下标,这样循环出来后,最小值和最大值下标都找到了,然后交换;Left ++ ,Rgiht -- 直到 Left 遇到 Right 就结束 循环, 数组也就 排序好了
代码:
public void selectSort1(int[] array) {
//双层循环,里层循环未排序的最小值,外层循环控制排序好的数字;
int left = 0;
int right = array.length - 1;
while(left < right) {
int minIndex = left;
int maxIndex = left;
for (int i = left + 1; i <= right; i++) {
if(array[minIndex] > array[i]) {
minIndex = i;
}
if (array[maxIndex] < array[i]){
maxIndex = i;
}
}
//这个循环出来之后,MinIndex 里面存储的就是数组的最小值的下标,MaxIndex里面就是最大值的下标
//交换即可
swap(array,minIndex,left);
//交换完成后,这里要判断 max和最小值交换位置是否相同,如果相同必须重置maxIndex的位置;
if(maxIndex == left) {
maxIndex = minIndex;
}
swap(array,maxIndex,right);
left++;
right--;
}
}
特点:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用2. 时间复杂度: O(N^2)3. 空间复杂度: O(1)4. 稳定性:不稳定
四、堆排序
(13条消息) [日常一练] 最小K个数,堆排序_Autumn60的博客-CSDN博客
特点:
1. 堆排序使用堆来选数,效率就高了很多。2. 时间复杂度: O(N*logN)3. 空间复杂度: O(1)4. 稳定性:不稳定
五、冒泡排序
根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特 点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
也可以理解为:
对无序区从前向后依次将相邻记录的关键字进行比较然后交换
代码:
public void bubbleSort(int[] array) {
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length -1 -i; j++) {
if(array[j] > array[j+1]) {
swap(array,j,j+1);
}
}
}
}
特点:
1. 冒泡排序是一种非常容易理解的排序2. 时间复杂度: O(N^2)3. 空间复杂度: O(1)4. 稳定性:稳定
六、快排
思想:
快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为: 任取待排序元素序列中的某元 素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止 。
挖坑法: 图例
这里定义的tmp以Left为基准 简写L ,Right 简写R,从R开始找比12小的R--
0下标的值放到tmp里面,以left为基准从右边开始找比基准小的,也就是R--;
这里找到比基准小的值,放到Left下标;然后Left找比基准大的; L++;
找到大的之后再去换到右边,再从R--,找小的;
这样以此类推,直到R 和 L 相遇的时候,把基准放到L或R的位置,这样就会把比基准大的推到右边,比基准小的推到左边; 第一趟有序:
这样下来0下标就有序了,然后i ++ ,从第二个下标开始,以此类推,整个数组就会有序;
挖坑代码:
public void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
private void quick(int[] array, int begin, int end) {
if (begin >= end) {
return;
}
//每一趟都会有一个数字有序;直到排序完毕;
for (int i = 0; i < array.length; i++) {
partition(array,i,array.length-1);
}
}
private int partition(int[] array, int left, int right) {
int tmp = array[left];
//定位基准下标位置;
while (left < right) {
//嵌套的while循环判断条件必须是<= 如果有一样的元素,不写=号会死循环;
while (left < right && array[right] >= tmp) {
right--;
}
//这里找到比tmp大的之后right停下来,和 left 交换,即可把小值放到 数组左边
swap(array, left, right);
//这里是找大值,放到右边
//尽量把最小值和最大值往两边推,越排续越快;
while (left < right && array[left] <= tmp) {
left++;
}
//这里把大值放到右边去;
swap(array, left, right);
}
return left;
}
快排Hoare法:
思想 :
快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 "基准"(pivot)
(2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大
(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。
图解:
定义一个S为基准,找小于基准的值的和大于基准的值,找到交换;
L 找到最小值, R找到最大值并且交换;
L 和 R 没相遇之前, 一直找
L 和 R相遇之后,再交换基准的值
注意事项;左边做基准,要从右边开始走,如果左边做基准,会把大于基准的值放到左边,会出现错误;
代码示例:
public int partition(int[] array, int left, int right) {
int tmp = array[left];
//定位基准下标位置;
int current = left;
while (left < right) {
//嵌套的while循环判断条件必须是<= 如果有一样的元素,不写=号会死循环;
while (left < right && array[right] >= tmp) {
right--;
}
while (left < right && array[left] <= tmp) {
left++;
}
//把 小于基准 和 大于基准 的值往两边推;
//把 小于基准的值 和 大于基准的值交换位置;
swap(array, left, right);
}
//再和基准进行交换,就可以把比 基准 小的放左边, 把 基准 大的放右边
swap(array, left, current);
return left;
}
优化:
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。
最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列
优化后分组结果:一次砍一半;效率翻倍!!
三数取中法:
找一个最大值,找一个最小值,再找中间的值,每一次递归都取中间数字做基准,效率就会提升很大,相当于二分了;
有了中间值之后,让中间值和默认的Left(horea默认基准)下标交换 ,让中间值成为新的基准;
代码:
public void quick(int[] array, int begin, int end) {
if (begin >= end) {
return;
}
System.out.println("begin : " + begin + " end : " + end);
//三数取中法,取到left和right 中间的数字即可;
int index = midThree(array, begin, end);
//这里的begin就会找到三数取中法找到的中间的数字;
//因为left是基准,只需要把left 基准 改为 中间值,也就是index就可以提高效率
swap(array, index, begin);
int pivot = partition2(array, begin, end);
quick(array, begin, pivot - 1);
quick(array, pivot + 1, end);
//挖可迭代,每一趟都会有一个数字有序,直到数组有序;
/* for (int i = 0; i < array.length; i++) {
partition(array,i,array.length-1);
}*/
}
/**
* 三数取中法,快速排序的优化;
*/
public int midThree(int[] array, int left, int right) {
//要取到中间的数,就要去遍历整个数组;
int mid = (left + right) / 2;
//假设left值刚好是中间值;
if (array[left] < array[right]) {
if (array[left] > array[mid]) {
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;
}
}
}
减少递归次数优化:
一棵树的最后两层大约有所有节点的70% 的节点,递归会花费大量时间,减少递归就可以了,当在递归的过程中,end 和 begin 区间, 数据 非常少的时候可以直接使用插入排序;
代码: 三数取中 + 减少递归
private void quick(int[] array, int begin, int end) {
if (begin >= end) {
return;
}
//减少递归次数的优化;
if (end - begin <= 10) {
insertSort222(array,begin,end);
return;
}
System.out.println("begin : " + begin + " end : " + end);
//三数取中法,取到left和right 中间的数字即可;
int index = midThree(array, begin, end);
//这里的begin就会找到三数取中法找到的中间的数字;
//因为left是基准,只需要把left 基准 改为 中间值,也就是index就可以提高效率
swap(array, index, begin);
int pivot = partition2(array, begin, end);
quick(array, begin, pivot - 1);
quick(array, pivot + 1, end);
//挖可迭代,每一趟都会有一个数字有序,直到数组有序;
/* for (int i = 0; i < array.length; i++) {
partition(array,i,array.length-1);
}*/
}
/**
* 三数取中法,快速排序的优化;
*
* @param array
* @param left
* @param right
* @return
*/
public int midThree(int[] array, int left, int right) {
//要取到中间的数,就要去遍历整个数组;
int mid = (left + right) / 2;
//假设left值刚好是中间值;
if (array[left] < array[right]) {
if (array[left] > array[mid]) {
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;
}
}
}
/**
* 倒数一两层快排节点优化;
* 减少递归次数的优化;
* @param array
*/
private void insertSort222(int[] array,int left,int end) {
//这里end 是 -1 之后的所以取 = ;
for (int i = left+1; i <= end; i++) {
//假设i下标是最小值,和i后边的每一位元素互相比较,直到i走到最后一位就比较完毕了
int tmp = array[i];
for (int j = i - 1; j >= left; j--) {
if (tmp < array[j]) {
array[j + 1] = array[j];
array[j] = tmp;
} else {
break;
}
}
}
}
非递归实现快排: 用栈来实现:
/**
* 非递归快排
* 找基准,放栈内,找基准,再比较,然交换,栈为空,序已成;
* @param array
* @param begin
* @param end
*/
public void dequeQuick(int[] array,int begin, int end) {
Deque<Integer> stack = new LinkedList<>();
int left = 0;
int right = array.length -1;
int pivot = partition(array, begin, end);
//现在是有了基准然后呢? 如果左边有最少两个元素就放进栈内;
//分左段 的区间, 也就是 0 到 pivot -1 ,右边的区间就是 pivot - right;
if(left +1 < pivot) {
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 = partition(array, left, right);
if(left +1 < pivot) {
stack.push(left);
stack.push(pivot-1);
}
if(pivot < right -1) {
stack.push(pivot + 1);
stack.push(right);
}
}
}
private int partition(int[] array, int left, int right) {
int tmp = array[left];
//定位基准下标位置;
while (left < right) {
//嵌套的while循环判断条件必须是<= 如果有一样的元素,不写=号会死循环;
while (left < right && array[right] >= tmp) {
right--;
}
//这里找到比tmp大的之后right停下来,和 left 交换,即可把小值放到 数组左边
swap(array, left, right);
//这里是找大值,放到右边
//尽量把最小值和最大值往两边推,越排续越快;
while (left < right && array[left] <= tmp) {
left++;
}
//这里把大值放到右边去;
swap(array, left, right);
}
return left;
}
特点:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
5.可以采用三数取中法来进行优化;
6.快排有 挖坑法, horea 法, 前后指针法(本文只写了前两种)
七、归并排序
归并排序,是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使 子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
思想 :
如上图。思路比较简单,就是对数组进行不断的分割,分割到只剩一个元素,然后,再两两合并起来。
代码示例:
public void merge(int[] array) {
mergeSort( array,0,array.length-1);
}
private void mergeSort(int[] array,int left ,int right) {
if (left == right) {
return;
}
int mid = (left + right) / 2;
mergeSort(array, left, mid);
mergeSort(array,mid+1, right);
merge1(array, left, right, mid);
}
private void merge1(int[] array,int stact,int end ,int mid) {
int s1 = stact;
//int e1 = mid;
int s2 = mid + 1;
//int e2 = end;
int k = 0;
int [] tmp = new int[end - stact + 1];
while(s1 <= mid && s2 <= end) {
if(array[s1] <= array[s2]) {
tmp[k++] = array[s1++];
} else {
tmp[k++] = array[s2++];
}
}
while(s1 <= mid ) {
tmp[k++] = array[s1++];
}
while(s2 <= end) {
tmp[k++] = array[s2++];
}
for (int i = 0; i < tmp.length; i++) {
array[i+stact] = tmp[i];
}
}
非递归实现归并:
图示思想: 一个 一个 排有序, 然后两个两个排有序, 以此递归,知道 组数大于或等于 数组长度,即可将 数 组 排 有 序
代码:
public void merge2(int[] array) {
int gap = 1;
while(gap < array.length) {
for (int i = 0; i < array.length; i += gap * 2) {
int left = i;
int mid = left + gap -1;
//这里的mid 可能会越界
if(mid >= array.length) {
mid = array.length-1;
}
int right = mid + gap;
//这里的Right 可能会越界
if(right >= array.length) {
right = array.length-1;
}
merge1(array,left,right,mid);
}
gap *= 2;
}
}
private void merge1(int[] array,int stact,int end ,int mid) {
int s1 = stact;
//int e1 = mid;
int s2 = mid + 1;
//int e2 = end;
int k = 0;
int [] tmp = new int[end - stact + 1];
while(s1 <= mid && s2 <= end) {
if(array[s1] <= array[s2]) {
tmp[k++] = array[s1++];
} else {
tmp[k++] = array[s2++];
}
}
while(s1 <= mid ) {
tmp[k++] = array[s1++];
}
while(s2 <= end) {
tmp[k++] = array[s2++];
}
for (int i = 0; i < tmp.length; i++) {
array[i+stact] = tmp[i];
}
}
特点:
1. 归并的缺点在于需要O(N)的空间复杂度,
应用场景: 归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定
[总结] :
此处的冒泡是最好时间复杂度是 基于优化的情况下的, 而 希尔 只需要记住 平均复杂度 即可,因为没科学家研究出来;