2024年常见排序算法基本原理及实现(快排,归并,堆排,直接插入(1),2024年最新上岸蚂蚁金服

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

public static void insertSort(int[] array) {
if(array==null) return;
for (int i = 1; i < array.length; i++) {
int j = i - 1;
int tem = array[i];
while (j >= 0) {
if (array[j] > tem) {
array[j+1] = array[j];
j–;
//i–;
} else {
break;
}
}
array[j + 1] = tem;
}
}


### 2.3性能分析


**时间复杂度**:  
 最好:**O(n)**,数据有序的情况下,只是遍历了一遍数组,最坏:**O(n^2),**数据逆序的情况下既要遍历数组也要比较数组。空间复杂度**O(1)**,开辟了固定的数组大小。稳定性:稳定。  
 **插入排序的特点:**  
 `初始数据越接近有序,时间效率越高。`


## 3.希尔排序


### 3.1 原理


希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序序列中所有记录分成我们选定的这个整数个组,**从第一个元素开始分组,两个数据之间的距离为每组元素个数的分在同一个组,并对每一组内的记录进行直接插入排序。然后重复上述分组和排序的工作。最后将所有的元素分在一个组内,对最后一个组再进行最后的排序即可。**希尔排序是对直接插入排序的优化,重复的进行分组之后又排序,先使局部有序之后再使整体有序。


![在这里插入图片描述](https://img-blog.csdnimg.cn/3e0c946289a4497a903efdf5687d4f93.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASXNjaGFuZ2Vk,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)  
 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。


### 3.2性能分析:


由严蔚敏老师的《数据结构》可知希尔排序的时间复杂度和所取得“增量”有关系,时间复杂度是“增量”的一个函数,但目前一个完美的增量序列如何确定还没有决解,由实验及部分数据可知希尔排序的时间复杂度为O(n1.3)-O(n1.5).空间复杂度为O(n).对于增量的取法,书中描述为取一个除了一之外没有公因子的数字作为增量也就是素数。


### 3.3代码实现



public static void shell(int[] array,int gap) {
//参数判断
if(array == null) return;
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;
}

}

public static void shellSort(int[] array) {
int gap = array.length;//初始增量为数组长度
while (gap > 1) {
shell(array,gap);
gap = gap/2;//1
}
shell(array,1);
}


由于我们所给的数据个数是不确定的,所以无法找到一个通用的式子计算出我们的增量让它为素数,所以只要遵循合理的将数组分组,每次分组时增量逐渐缩小,最后分为一组即可。上面的代码和直接插入排序的基本一样,只是这里的i=gap,下的内容也相应地换一下。


## 4. 选择排序


### 4.1直接选择排序-原理


![在这里插入图片描述](https://img-blog.csdnimg.cn/82611f83fe254090859e0f11d8b63c2b.gif#pic_center)  
 选择排序,选择排序直接就升序来说,假设第一个元素是最小的,依次和后面的元素比较假设第一个元素比比较的元素大交换两者的位置,不大于不做任何操作,比较下一个元素,当数组元素比较完后,第一趟比较结束,第一个元素为最小值,第二趟比较假设第二个元素是最小的依次和后面的元素比较和交换重复第一步,当比较了n-1趟后元素就是有序的了。


### 4.2实现



public static void selectSort(int[] array) {
if(array==null||array.length<2){
return;
}
for (int i = 0; i <array.length-1 ; i++) {
int min=i;//默认第一个元素最小
for (int j = i+1; j <array.length; j++){
if(array[min]>array[j]){
min=j;

            }
        }
        if(min!=i){
              int tem=array[i];
                array[i]=array[min];
                array[min]=tem;
        }
    }
}

### 4.3性能分析:


时间复杂度\*\*:O(n^2)\*\* 不管有序还是无序的情况,可以适当的优化下,就是在比较的时候如升序,前面的元素比后面的大时不要每次交换,用一个临时变量记录最小值,当一趟遍历完后再交换,但时间复杂度任然是**O(n^2)** 空间复杂度:**o(1)**, 稳定性:不稳定的排。


## 5.堆排序


### 5.1原理


![在这里插入图片描述](https://img-blog.csdnimg.cn/ecb85b0d93614a8ea52156ecb429682f.gif#pic_center)


基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数,\*\*堆逻辑上是一棵完全二叉树  
 满足任意结点的值都大于等于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆反之,则是小堆,或者小根堆,或者最小堆,子树中节点的值没有关系。\*\*就升序而言,我们创建一个大堆,每次堆顶的元素都是最大的,我们把它和堆的最后一个元素交换,堆的末尾元素就是最大值,也是数组的最后一个元素就是最大值,有序的元素下次交换调整时排除在排序范围内,之再调整交换堆使它继续为大根堆,之后重复上面的操作,第二次得到第二大元素,直到排序到堆顶元素为止。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/63366ff493eb44e69768e10363863c3a.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASXNjaGFuZ2Vk,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)  
 `注意:` **排升序要建大堆;排降序要建小堆。**  
 再简单总结下堆排序的基本思路:



> 
> a.将无序序列构建成一个堆,根据升序降序需求选择大根堆或小根堆;  
>  b.将堆顶元素与末尾元素交换,将最大元素或者最小元素"沉"到数组末端;  
>  c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个数组有序。
> 
> 
> 


### 5.2实现



public static void shiftDown(int[] array,int parent,int len) {
int child = 2*parent+1;
while (child < len) {
if(child+1 < len && array[child] < array[child+1]) {
child++;
}
//child下标 表示的就是左右孩子最大值的下标
if(array[child] > array[parent]) {
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
parent = child;//如果子树不为空,操作子树就行,改变的那颗子树
child = 2*parent+1;
}else {
break;
}
}
}

public static void createBigHeap(int[] array) {
for (int i = (array.length-1-1)/2; i >= 0 ; i–) {
shiftDown(array,i,array.length);
}
}
/**
* 时间复杂度:O(N*logn)
* 空间复杂度:O(1)
* 稳定性:不稳定
* @param array
*/
public static void heapSort(int[] array) {
createBigHeap(array);
int end = array.length-1;
while (end > 0) {
int tmp = array[0];
array[0] = array[end];
array[end] = tmp;
shiftDown(array,0,end);
end–;
}
}


### 5.3性能分析


时间复杂度:\*\*O(n \* log(n)),\*\*可以看成一颗完全二叉树,每个节点都要进行交换,交换之后要进行向下调。整空间复杂度:O(1)


## 6冒泡排序


### 6.1原理


![在这里插入图片描述](https://img-blog.csdnimg.cn/cb84bac2614948f7a0f788e4a026f7d0.gif#pic_center)  
 在待排序序列中,通过相邻数的比较,逐渐的将较大的数据移动到待排序序列的后面,持续这个过程,直到数组整体有序。假设有n个数据只需要比较`n-1`趟,每一次冒泡出无序序列中的最大值,对于第一趟的比较比较的次数依然是n-1次,之后每趟比较的次数依次减少1。直到n-1趟比较完,数据有序了,下面的代码进行了一些优化,当待排序序列本身有序的时候,完成了一次排序就结束了。



public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length-1; i++) {
boolean flg = false;//做一个标记防止数据本身有序
for (int j = 0; j < array.length-1-i; j++) {//每一趟的比较次数都在减少所以要减i
if(array[j] > array[j+1]) {
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flg = true;
}
}
if(flg == false) {
break;
}
}
}


### 6.2性能分析


时间复杂度:O(N^2)数据无序的情况下,O(N)数据有序的情况下,空间复杂度O(1),稳定性:稳定的排序。


## 7快速排序(重要)


### 7.1原理-总览


![在这里插入图片描述](https://img-blog.csdnimg.cn/40b1cb3d9df647ba8b869c23b3a2ddb5.gif#pic_center)


1. 从待排序区间选择一个数,作为基准值(pivot);
2. Partition:为划分函数,遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
3. 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间的长度 == 0,代表没有数据。


#### 7.1.1挖坑法:



public static int partition(int[] array,int start,int end) {
int tmp = array[start];
while (start < end) {
//1、先判断后面
while (start < end && array[end] >= tmp) {//找到一个小于基准点的值
end–;
}
//1.1 后面的给start array[start] = array[end]
array[start] = array[end];

        //2、再判断前边
        while (start < end && array[start] <= tmp) {
            start++;
        }
        //2.1 把这个大的给end array[end] = array[start]
        array[end] = array[start];
    }
    //start=end
    array[start] = tmp;
    return start;
}
//类似二叉树搜索树的前序遍历,基准点就是二叉树的根结点
public static void quickSort(int[] array,int left,int right) {
    if(left >= right) {//递归结束的条件
        return;
    }
    int pivot = partition(array,left,right);
    quickSort(array,left,pivot-1);
    quickSort(array,pivot+1,right);
}


public static void main(String[] args) {
    int []arr={6,0,1,2,7};
    System.out.println(Arrays.toString(arr));
    quickSort(arr,0,arr.length-1);
    System.out.println(Arrays.toString(arr));

}

假设我以数组5,1,2,4,3,6为例排升序,大致过程如下,将就看下,第一次以5为基准一次划分如下:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/78478d9d444448969e7d33cfd554e546.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASXNjaGFuZ2Vk,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)  
 接着再以同样的方式,先划分左再划分右一直递归下去,有一个数字的序列不用划分了,默认是有序的。下面以3为基准的划分,以2基准划分,以1基准划分划分完之后,最开始的基准的左边是不是有序了,再开始右边排序,之后整个序列就有序了。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/6ed3306088d04d12a45c48a6f9b5c4c7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASXNjaGFuZ2Vk,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)


#### 7.1.2:Hoare 法:


基本思路和挖坑法一致,只是不再进行进行赋值,而是进行两个数的交换,实现了一个交换函数:



public static void quickSort1(int[] array,int left,int right) {
if(left >= right) {//递归结束的条件
return;
}
int pivot = partition1(array,left,right);
quickSort(array,left,pivot-1);
quickSort(array,pivot+1,right);
}
public static int partition1(int[] array, int left, int right) {
int i = left;
int j = right;
int pivot = array[left];
while (i < j) {
while (i < j && array[j] >= pivot) {
j–;
}
while (i < j && array[i] <= pivot) {
i++;
}
swap(array, i, j);
}
swap(array, i, left);
return i;
}
// //交换的方法
public static void swap(int[]array,int i,int j){
int tem=array[i];
array[i]=array[j];
array[j]=tem;
}


### 7.2性能分析


时间复杂度:可以把快速排序看成二叉树的前序遍历,每个节点做为基准都遍历了都排好序了,整个数组就有序了,最好情况下**O(n \* log(n))**,**每次递归都将待排序序列均匀分割,树的深度和每一层的遍历区间相乘。最坏情况下O(n^2),此时数据有序,就是一个单分支的树了,空间复杂度最坏情况下:O(n)。稳定性:不稳定。**


### 7.3快速排序的优化


对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。最理想的方法是,选择的基准恰好能把待排序序列分成`两个等长的子序列。`


#### 7.3.1固定位置法:


基本的快速排序选取第一个或最后一个元素作为基准。但不是一种好方法,\*\*因为如果当数据有序的时候,这样的算法效率很低,每次排序完只能排除一个数字,时间复杂度很大,\*\*就像上面写的快速排序就是固定位置法的快速排序,因此下面介绍效率更高的两种基准选取的方法。


#### 7.3.2随机选取基准法(了解)


**思想:利用随机数,取待排序列中任意一个元素作为基准**



public static void quickSort(int[] array,int left,int right) {
if(left >= right) {//递归结束的条件
return;
}

   // 1、随机选择基准-》先将left下标的值换一下
    //rand 交换array[left] array[rand]
    Random random = new Random();
    int rand = random.nextInt(right-left)+left+1;
  swap(array,left,rand);
    int pivot = partition(array,left,right);
    quickSort(array,left,pivot-1);
    quickSort(array,pivot+1,right);
}

随机选取基准法,较固定位置法效率有所改变,但存在极大的偶然性,不排除你每次选的基准是待排序序列里面的最大值或者最小值,也不能将待排序序列进行均匀的分割。


#### 7.3.3三数取中(median-of-three)(优化有序的数据)


其实最好的做法就是找到待排序数组的中间值以它进行划分,当这样是很难得到的,一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准值。显然使用三数中值分割法消除了预排序输入的不好情形。



public static void medianOfThree(int[] array,int left,int right) {
int mid = (left+right)/2;
if(array[mid] > array[left]) {
int tmp = array[mid];
array[mid] = array[left];
array[left] = tmp;
}//array[mid] <= array[start]
if(array[left] > array[right]) {
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}//array[start] <= array[right]
if(array[mid] > array[right]) {
int tmp = array[mid];
array[mid] = array[right];
array[right] = tmp;
}
//array[mid] <= array[start] <= array[right]
}
//类似二叉树搜索树的前序遍历,基准点就是二叉树的根结点
public static void quickSort(int[] array,int left,int right) {
if(left >= right) {//递归结束的条件
return;
}
//2、三数取中法
medianOfThree(array,left,right);
int pivot = partition(array,left,right);
quickSort(array,left,pivot-1);
quickSort(array,pivot+1,right);
}


#### 7.3.4使用插入排序


我们知道插入排序的特点是,数据越有序排序速度越快,当待排序序列的长度分割到一定大小后,待排序序列长度在5~20之间任一截止范围都可以使用直接插入排序,而不使用快速排序。



public static void insertSort2(int[] array,int left,int right) {
//参数判断
if(array == null) return;
for (int i = left+1; i <= right; i++) {
int tmp = array[i];
int j = i-1;
for (; j >= left ; j–) {
if(array[j] > tmp) {
array[j+1] = array[j];
}else {
break;
}
}
array[j+1] = tmp;
}
}
//3、递归执行到一个区间之后 进行直接插入排序,加入到导快速排序之中
if((right - left + 1) <= 20) {
insertSort2(array,left,right);
return;//
}


#### 7.3.5快速排序的非递归


我们通常见到的快速排序为递归版本,并没有关注非递归的方式。但非递归版本也是很好实现的,因为递归的本质是`栈`。在非递归实现的过程中,借助栈来保存中间变量就可以实现非递归了。下面的非递归也瞅瞅(\* ̄︶ ̄)。



public static void quickSort2(int[] array) {
Stack stack = new Stack<>();
int start = 0;
int end = array.length-1;
int pivot = partition(array,start,end);
if(pivot > start+1) {
stack.push(start);
stack.push(pivot-1);
}
if(pivot < end-1) {
stack.push(pivot+1);
stack.push(end);
}
while (!stack.empty()) {
end = stack.pop();

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

+1) {
stack.push(start);
stack.push(pivot-1);
}
if(pivot < end-1) {
stack.push(pivot+1);
stack.push(end);
}
while (!stack.empty()) {
end = stack.pop();

[外链图片转存中…(img-B0Vv5bNY-1715595538855)]
[外链图片转存中…(img-swJZzvk0-1715595538856)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值