目录
系列文章目录
刷题笔记(一)–数组类型:二分法
刷题笔记(二)–数组类型:双指针法
刷题笔记(三)–数组类型:滑动窗口
刷题笔记(四)–数组类型:模拟
刷题笔记(五)–链表类型:基础题目以及操作
刷题笔记(六)–哈希表:基础题目和思想
刷题笔记(七)–字符串:经典题目
刷题笔记(八)–双指针:两数之和以及延伸
刷题笔记(九)–字符串:KMP算法
刷题笔记(十)–栈和队列:基础题目
刷题笔记(十一)–栈和队列:Top-K问题
前言
怎么说呢,这一部分其实在原本的刷题计划里是没有的,但是我这里临时把它加上的原因就是因为上篇博客给我的印象比较深,我感觉自己关于排序算法这里好多东西应该是写不出来的。所以这一篇博客也是复习。
一丶插入排序和其进阶希尔排序
<1>插入排序
1.具体实现
插入排序是什么呢?
插入排序就是把一个数字插入到一个已经排好了的有序数组当中。注意有一个前提:已经排好了的。
所以从这一点来思考,可能是有些抽象,那么我们用一个具体的场景
我们想把这个7插入到前面的有序数组中去,那么就要从后往前开始开始遍历(PS:从前往后可不可以?当然可以!),每找到一个大于它的数字就要把该数字往后挪,然后直到找到小于它的第一个数字,然后把后面的那个数字赋值为当前数字7。
是不是有点懵?那么我们来一个具体的实现场景,我们来排一个数组[4,2,8,6,9,1,3,5,0,7]
。然后排序的过程如下:
PS:(这里是从下标i = 1开始排序,不是从i = 0)
对应的代码如下:
public class 插入排序 {
public static void insertSort(int[] arr) {
for(int i = 1;i < arr.length;i++){
int key = arr[i];//定义当前需要比较的元素
//然后要和i下标之前的元素进行比较,如果说元素比当前元素大,那么就往后挪一位
int end = i - 1;
while(end >= 0 && key < arr[end]){
arr[end + 1] = arr[end];
end--;
}
//这一步走完了之后,当前end下标所对应的值肯定是小于key的,所以对其后一位进行赋值
arr[end + 1] = key;
}
}
public static void main(String[] args) {
int[] arr = {4,2,8,6,9,1,3,5,0,7};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
}
最后的运行结果如下:
2.关于复杂度
空 间 复 杂 度 \color{red}空间复杂度 空间复杂度
首先从空间复杂度来讨论,它并没有借用额外的空间,所以空间复杂度是O(1)
时 间 复 杂 度 \color{red}时间复杂度 时间复杂度
最坏的情况也就是当前要排序的数组是一个降序的数组,比如说[9,8,7,6,5,4,3,2,1]
这种的,当为降序的数组的时候,其时间复杂度是一个等差数列的n项和,用大O阶表示法来进行表示的话O(n^2)
<2>希尔排序
在插入排序的进阶上,是希尔排序。为什么说是进阶呢?首先我们上面也说了,希尔排序的适合场景是接近有序的数组
,越接近有序,这个算法的效率就越高,所以我们如果在排序前对数组进行处理使其变得有序的话可以大大提高插入排序的效率。而这种思想就是我们的希尔排序。
具体的实施的话就是对数组进行分组,什么意思呢?
就像上面这样,然后对每一组进行插入排序,再慢慢缩小组的范围。这样我们处理的数组会越来越接近有序。但是这里学问是怎么还是比较多的
1.希尔排序就是对插入排序的直接优化
2.当间隔 > 1的时候其实都是对数组的优化,当间隔 == 1的时候,数组就是已经很接近有序的了,这样排序就会很快。
3.希尔排序的时间复杂度不好说,因为不同选择导致的时间复杂度是不同的,具体根据实际情况来看。
实际代码如下:
public class 希尔排序 {
//希尔排序
public static void shell(int[] arr){
int gap = arr.length;
//首先要明白,当size == 1或者小于1的时候就没有必要进行排序了,此时已经是有序的了。
while(gap > 1){
//这里间隔选取还是要注意一下的,可以是 gap / 2,也可以是我下面写的这种
gap = gap / 3 + 1;
//以下代码就是插入排序基础上稍稍改造了一下
for(int i = gap;i < arr.length;i++){
int key = arr[i];
int end = i - gap;
while(end >= 0 && arr[end] > key){
arr[end + gap] = arr[end];
end -= gap;
}
arr[end + gap] = key;
}
}
}
public static void main(String[] args) {
int[] arr = {4,2,8,6,9,1,3,5,0,7};
shell(arr);
System.out.println(Arrays.toString(arr));
}
}
注意这里插入排序的思想其实是很奇妙的,我这里是刷题笔记,如果是初学者可能看的有点懵,具体的可以看我以前写的博客
算法–插入排序
二丶选择排序和其进阶堆排序
<1>选择排序
1.具体实现
什么是选择排序呢?其实说起来很方便,就是每次选出最大的数值放在数组末尾。用图来说一下
也很简单,就是每一趟选出最大的数字,然后和最后一个位置进行数字的交换。代码如下:
public class 选择排序 {
public static void selectSort(int[] arr){
int end = arr.length - 1;//定义end指针来进行元素的交换
while(end > 0){//如果end == 0就没有交换的比较了
int key = 0;//这里key是当前交换的最大元素的下标
for (int i = 0; i <= end; i++) {//寻找最大元素
if(arr[i] > arr[key]){
key = i;
}
}
if(end != key){//如果不相等,那就交换
int tmp = arr[end];
arr[end] = arr[key];
arr[key] = tmp;
}
end--;//当前交换完毕,所以end--
}
}
public static void main(String[] args) {
int[] arr = {4,2,8,6,9,1,3,5,0,7};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
}
2.关于复杂度
关 于 时 间 复 杂 度 \color{red}关于时间复杂度 关于时间复杂度
这里时间复杂度,每次遍历都是在原来的基础上减1,所以也就是n + (n - 1) + (n - 2)…所以最后是一个等差数列的和。也就是时间复杂度是O(n^2)。
关 于 稳 定 性 \color{red}关于稳定性 关于稳定性
稳不稳定看什么呢?就是看交换元素是不是搁着元素交换的,如果隔着元素交换那就是不稳定的,所以当前排序算法不稳定。
<2>堆排序
1.具体实现
堆排序其实前面我们的TOP-K刚做过,就是优先级队列。那么堆排序和选择排序有什么关系呢?
选择排序其实有一个很大的问题,就是它的重复比较太多了,所以我们需要一个元素,能瞬间找到最大元素,所以我们的堆排序应运而生。
以下是堆排序的代码:
public class 堆排序 {
public static void main(String[] args) {
int[] arr = {4,2,8,6,9,1,3,5,0,7};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr){
//当前size节用来向下调整获得大根堆
int size = arr.length;
for (int i = (size - 1)/ 2; i >= 0; i--) {
//首先进行一个堆的调整,使得当前堆为大根堆
shiftDown(arr,size,i);
}
//当前end用来进行堆排序,要实时更新end的值
//其实主要思想就是建立大根堆之后不断交换根节点和最后一个节点,然后不断缩小end的值
int end = size - 1;
while (end > 0){
swap(arr,0,end);
shiftDown(arr,end,0);
end--;
}
}
public static void shiftDown(int[] arr,int size,int parent){
int child = parent * 2 + 1;//这是左孩子
while(child < size){
//进行判断:左右孩子哪个大,哪个就用来交换
if(child + 1 < size && arr[child + 1] > arr[child]){
child = child + 1;
}
//如果说孩子的值大于父值,那就交换值,然后更新parent的节点值
if(arr[child] > arr[parent]){
swap(arr,child,parent);
parent = child;
child = parent * 2 + 1;
}else{
return;
}
}
}
public static void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
2.关于复杂度
关 于 时 间 复 杂 度 \color{red}关于时间复杂度 关于时间复杂度
单个元素往下排序的元素复杂度是log₂n。
n个元素,那么时间复杂度就是O(N*log₂n)
关 于 稳 定 性 \color{red}关于稳定性 关于稳定性
前面说了,稳不稳定怎么看?就是看有没有搁着元素交换。那么有没有呢?当然是有的
三丶冒泡排序和快速排序
<1> 冒泡排序
1.具体实现
冒泡排序是我们大学第一个学的排序算法,实现原理也很简单,一轮实现一个数字的排序。每次都是从头开始遍历,然后如果当前下标的后一位的值小于当前下标的值就进行交换。
public class 冒泡排序 {
public static void bubbleSort(int[] arr){
//表示要遍历的次数
for (int i = arr.length; i > 0; i--) {
//当前遍历的形式
for (int j = 0; j < i - 1; j++) {
//如果说当前下标的值大于后一个
if(arr[j] > arr[j + 1]){
swap(arr,j,j+1);
}
}
}
}
public static void swap(int[] arr,int i,int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void main(String[] args) {
int[] arr = {4,2,8,6,9,1,3,5,0,7};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
}
2.关于复杂度
稳 定 性 \color{red}稳定性 稳定性
因为没有隔着元素交换,所以它是稳定的
关 于 时 间 复 杂 度 \color{red}关于时间复杂度 关于时间复杂度
最坏的情况就是比一次交换一次,所以最后其实还是一个等差数列的和,也就是O(n^2)。
<2>快速排序
1.具体实现–Hoare版本
哇,终于来到了这个贼有意思的排序,快排的思想真的很nice,是在递归上进行实现的。我这里分成了两个版本,笔者在第二次复习的时候觉得自己第一次写的太过于臃肿,所以有了在一起写的版本。
快速排序是一个递归实现,如果单独把递归中某一层拉出来说,那就是找一个值key,然后把所有小于key的值放在右边,把所有大于key的值放在右边。那总的实现就是一直递归到最小元素集合,然后排好之后一层一层往回走,接着就有了有序版本。所以这样就有了我们的基础代码版本
如下:
public class 快速排序1 {
//hoare版本
public static void quickSort(int[] arr,int left,int right){
//首先要明白,如果说当前要排序元素不大于1,那就没有排序的必要
if(right - left > 1) {
int index = partion1(arr, left, right);
quickSort(arr,left,index);
quickSort(arr,index+1,right);
}
}
public static int partion1(int[] arr,int left,int right){
//这一步其实是可以省略的,但是为了后面的优化代码这一步我保留
int index = right - 1;//当前下标是比较元素的下标
int key = arr[index];
int begin = left;
int end = index;
while(begin < end){
while (begin < end && arr[begin] <= key){
begin++;//这一个while循环走完之后确保begin下标对应元素大于key
}
while (begin < end && arr[end] >= key){
end--;//这一个while循环走完之后确保end下标对应元素小于key
}
//end > begin的时候就可以进行交换
if(begin < end){
swap(arr,begin,end);
}
}
//最后把begin的位置填上key
swap(arr,begin,right-1);
return begin;
}
public static void swap(int[] array, int left, int right){
int temp = array[left];
array[left] = array[right];
array[right] = temp;
}
public static void main(String[] args) {
int[] arr = {4,2,8,6,9,1,3,5,0,7};
quickSort(arr,0,arr.length);
System.out.println(Arrays.toString(arr));
}
}
2.具体实现–挖坑法版本
这一版本的实现其实和上面的思想是很接近的,但是区别在哪里呢?就是我们一开始就会保存要比较的下标值,然后把标志值的位置假设为“坑”。前指针先从先往后遍历,找到大于key的值往坑填,填了之后当前前指针的位置就变成了“坑”。后指针再开始从后往前遍历找到一个小于key的值往坑里填,填完之后后指针的位置就变为“坑”。具体代码实现如下:
public class 快速排序2 {
public static void quickSort(int[] arr,int left,int right){
if(right - left > 1){
int index = partion2(arr,left,right);
quickSort(arr,left,index);
quickSort(arr,index+1,right);
}
}
public static int partion2(int[] arr,int left,int right){
//当前版本为挖坑法
//老规矩,还是先定义当前比较值的下标
int index = right - 1;
//然后是设置一系列的需要属性
int key = arr[index];
int begin = left;
int end = index;
while(begin < end){
//先从前往后找第一个大于key的值
while(begin < end && arr[begin] <= key){
begin++;
}
if(begin < end){
arr[end] = arr[begin];
}
//再从后往前找第一个小于key的
while(begin < end && arr[end] >= key){
end--;
}
if(begin < end){
arr[begin] = arr[end];
}
}
//最后把begin这个坑填上值
arr[begin] = key;
return begin;
}
public static void main(String[] args) {
int[] arr = {4,2,8,6,9,1,3,5,0,7};
quickSort(arr,0,arr.length);
System.out.println(Arrays.toString(arr));
}
}
3.具体实现–双指针版本
双指针版本算是这三个版本里面最有意思的一个,因为前两个都是从两端开始发起交换,但是双指针版本是一起从前往后走。这里图解一下
然后用文字阐述一下关键点
关于先后指针,我们用当前方法只要注意一点就好,就是前后指针之前的值必须要大于key!!!必须要大于key。
以下是代码部分:
public class 快速排序3 {
public static void main(String[] args) {
int[] arr = {4,2,8,6,9,1,3,5,0,7};
quickSort(arr,0,arr.length);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr,int left,int right){
if(right - left > 1){
int index = partion3(arr,left,right);
quickSort(arr,left,index);
quickSort(arr,index+1,right);
}
}
public static int partion3(int[] arr,int left,int right){
int index = right - 1;
//然后是进行双指针版本需要的数据处理
int key = arr[index];
int cur = left;//这是后指针
int pre = cur - 1;//这是前指针
while(cur < right){
//这是为了前后指针之间全部是大于key的值,然后有小于key的,就要往pre也就是前指针之前放
if(arr[cur] < key && ++pre != cur){
swap(arr,pre,cur);
}
cur++;
}
//最后判断一下pre往前走一位之后必定是大于key的值,此时交换pre和key的下标值
if(++pre != index){
swap(arr,index,pre);
}
return pre;
}
public static void swap(int[] arr,int i,int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
4.优化
这里优化的话,是从哪里优化呢?就是我们的index选择,如果读者们有仔细查看前面的代码的话,就可以发现其实我一直在书写着一行无意义的代码
int index = right - 1;
实际上这里并不是无意义的,这是我们要优化的就是这个index的选取。如果说当前的数组为 [9,8,7,6,5,4,3,2,1]
,这是一种啥情况?是不是就是时间复杂度最大的情况。所以为了防止这种极端情况,我们可以怎么优化呢?就是在前中后选三个元素,然后选择中间的这个值作为index
public static int getMiddle(int[] array, int left, int right){
int mid = left + ((right-left)>>1);
if(array[left] < array[right-1]){
if(array[mid] < array[left]){
return left;
}else if(array[mid] > array[right-1]){
return right-1;
}else{
return mid;
}
}else{
if(array[mid] > array[left]){
return left;
}else if(array[mid] < array[right-1]){
return right-1;
}else{
return mid;
}
}
}
所以加了这段代码之后,我们的index选取就要改变了
int index = getIndexOfMiddle(array, left, right);
完了嘛?当然没有!我们不可能说前中后三种情况选择各来一种快速排序算法书写。那么我们可以交换index和right - 1的值。
if(index != right-1){
swap(array, index, right-1);
}
然后其他地方都是一样的。
5.关于复杂度
稳 定 性 \color{red}稳定性 稳定性
它是隔着元素进行交换的,所以是不稳定的
时 间 复 杂 度 \color{red}时间复杂度 时间复杂度
先看这个图然后我再解释
就是每一层的遍历总次数 * 总层数。因为每一层无论你怎么分最后遍历总次数还是N,这里的层数其实就是第一n不断的除二除二除二一直到最后为1位置,所以这里的高度其实就是log₂n,最后这两个相乘就是我们的时间复杂度N * log₂n
四丶归并排序
1.具体实现
所谓归并排序,就是分治算法的体现。怎么说呢,我用一张图来解释吧。
首先就是把数组分组,然后对分组之后的各个分组继续分,分到不能分为止。然后从底层开始对这些数组进行排序,单独拉一层出来说
归并排序的主要地方不是对数组内的元素处理,而是用了一个额外的空间来存储数组内元素的排序。也就是说,主要的是合并的过程和分组的过程。它从上往下分,一直分到只有一个元素的时候,其实对单个元素来说,它就是有序的。所以合并过程就是主要的,也是归并排序的灵魂。所以这里难点就是理解递归分组+合并处理,要好好理解。
是不是有点懵?没关系,先看图
精髓就是这个两个数组合并的过程。
接下来先给出递归分组的代码
public static void mergeSort(int[] arr,int left,int right,int[] temp){
if(right - left > 1){//如果说当前要排序的元素总数大于1,那么就排,不然就返回
int mid = left + ((right - left) >> 1);
//开始进行分组
mergeSort(arr,left,mid,temp);//先分递归左边的
mergeSort(arr,mid,right,temp);//再分递归右边的
mergeSortDate(arr,left,mid,right,temp);//分好了之后就要合并数组了,这个合并是精髓
System.arraycopy(temp,left,arr,left,right - left);//这里把临时空间已经拍好了序的数组复制到arr数组里面。
}
}
分好了之后就要开始合并数组了
public static void mergeSortDate(int[] arr,int left,int mid,int right,int[] temp){
//首先定义好指针
int begin1 = left,end1 = mid;
int begin2 = mid,end2 = right;
int index = left;
//接着开始两个数组的合并
while(begin1 < end1 && begin2 < end2){
if(arr[begin1] >= arr[begin2]){
temp[index++] = arr[begin2++];
}else{
temp[index++] = arr[begin1++];
}
}
//最后剩谁就一直往临时空间填就行
while(begin1 < end1){
temp[index++] = arr[begin1++];
}
while(begin2 < end2){
temp[index++] = arr[begin2++];
}
}
这两个合起来就是我们的归并排序算法
当然我们还可以做一点小小的处理,不然这个用起来太麻烦了。也就是归并排序算法的方法重载
public static void mergeSort(int[] arr){
int[] temp = new int[arr.length];
mergeSort(arr,0,arr.length,temp);
}
2.关于复杂度
时 间 复 杂 度 \color{red}时间复杂度 时间复杂度
可以看出,这里其实就是一个二叉树的形式,和堆排序时间复杂度一样都是O(N*log₂n),但是两个不是一个东西嗷。
空 间 复 杂 度 \color{red}空间复杂度 空间复杂度
这里的空间复杂度因为每一次递归都是要重新创建一个temp数组,所以最后空间复杂度是O(N)
稳 定 性 \color{red}稳定性 稳定性
搁着元素排序,当然是稳定的啦
总结
终于完啦,但是好像还有两个没写是不是?一个是计数排序(非比较排序),还有一个是桶排序。这里的话有时间写篇博客补一补这两个比较特殊的吧,之后做题还是上面的这几个比较常用些,加油加油!