数据结构十大排序算法+二分查找(左程云 左神版 全文2W字+ word很大,你忍一下~~)
Posted DU777DU
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构十大排序算法+二分查找(左程云 左神版 全文2W字+ word很大,你忍一下~~)相关的知识,希望对你有一定的参考价值。
这是我在b站上2021最新左神数据结构算法全家桶这个视频上学习的排序算法,感觉挺不错的
在这里和大家分享一下
目录
1. 选择排序
1.1 选择排序的思想
参考左神画的图:
(1)从头(下标0位置)遍历一遍数组找到最小值,和下标位0位置上的数据交换,此时0位置的数据就固定是最小值了
(2)从下标1位置开始(0位置已经固定就不用考虑了)再次遍历一遍数组,找到最小值,和下标为1位置上的数据交换,此时1位置的数据也就固定了
(3)从下标2位置开始(0、1位置都已经固定)再次遍历一遍数组,找到最小值,和下标为2位置上的数据交换,此时2位置的数据也就固定了......
一直这样循环直到最后一个值,此时就完成了排序
1.2 时间复杂度
时间复杂度为O(N^2)
可以看出常数操作的次数为一个等差数列 aN^2 + bN +C 我们只考虑最高项就是N^2
一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
空间复杂度:O(1) 开辟的额外空间:数i , j 变量minIndex(每次循环都释放)
1.3 代码实现
左神的代码:
public static void selectionSort(int arr[]){
//如果数组为空或者数组长度小于2,那么不需要排序直接返回
if(arr == null || arr.length < 2){
return;
}
//数组长度大于等于2则开始排序
for (int i = 0; i < arr.length -1; i++){ // 从i ~ N-1上依次给数组重新赋值
int minIndex = i; //先假设i位置上最小
for (int j = i + 1; j < arr.length; j++){ // 从i ~ N-1上找最小值的下标
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
// i和最小值下标交换,则i位置上就是最小值,此时i就固定了,i++从下个位置继续
swap(arr, i, minIndex);
}
}
//交换数据
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
代码思想:
选择排序是从前往后排序
(1)先假设 i 位置上最小,赋给minIndex
(2)【内层循环】从(i ~ N-1)上找最小值,拿这个值和当前的minIndex比较 ,如果小则把这个值重新赋给minIndex,大则不动,再找下一个值,直到N-1
(3)【外层循环】内循环进行一次就确定了一个最小值,下一次就不用考虑这个值了,所以 i++
2. 冒泡排序
2.1 冒泡排序思想
依旧看左神画的图:
(1)数组的值在横线上面,索引在横线下面
(2)从索引0开始,依次进行两两比较直到最后一个数据。前一个位置比后一个位置小则不动,大则交换,整个数组比较完毕后,最后位置的值就是最大的,此时最后位置索引的值固定不变
(3)再次从索引0开始,依次进行两两比较,直到倒数第二个数据,再次比较完后此时倒数第二个索引的值也固定不变......
(4)一直循环直到索引位置1的值也固定,此时就排序完毕了
2.2 时间复杂度
时间复杂度为O(N^2)
可以看出常数操作的次数为一个等差数列 aN^2 + bN +C 我们只考虑最高项就是N^2
2.3 代码实现
左神的代码:
public static void bubbleSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(int end = arr.length - 1; end > 0; end--){ // 从0~end进行排序,每次排序固定最后一个位置,end--
for(int i = 0; i < end; i++){ //每次从0~end进行两两比较
if (arr[i] > arr[i + 1]){ //前一个位置比后一个位置大则交换
swap(arr, i, i+1);
}
}
}
}
//交换arr的i和j位置上的值,利用异或机制
public static void swap(int[] arr, int i, int j){
arr[i] = arr[i] ^ arr [j];
arr[j] = arr[i] ^ arr [j];
arr[i] = arr[i] ^ arr [j];
}
代码思想:
冒泡排序是从后往前排序
(1)先初始化一个end来指向最后一个数据,保存数组中值最大的数据
(2)【内层循换】找最大值,从(0~end)范围内进行两两比较,前一个位置比后一个位置大则交换,一轮循环完毕后end索引就保存的是最大值了
(3)【外层循环】内循环进行一次就确定了一个最大值,此时就不用考虑这个值了,所以给end--
2.4 利用异或机制交换数组
其中交换数组位置利用了异或机制(超级帅):
左神画的图解:
注:必须是两块不同内存的数据才可以用,相同内存异或会把数据洗为0
3. 插入排序
3.1 插入排序思想
(1)先做到0~0范围上有序,自然做到了
(2)要做到0~1范围上有序,指针指到索引1上,数据值和前面的依次比较,大则不动,小则交换,换到比前面数值都大或前面没数据时停止,比较完毕后0~1范围内就是有序的了
交换后:
(3) 要做到0~2范围上有序,指针指到索引2上,数据值和前面的依次比较,大则不动,小则交换,换到比前面数值都大或前面没数据时停止,比较完毕后0~2范围内就是有序的了
(4)要做到0~3范围上有序,指针指到索引3上,数据值和前面的依次比较,大则不动,小则交换,换到比前面数值都大或前面没数据时停止,比较完毕后0~3范围内就是有序的了......
交换后:
(5)重复循环直到最后一个数也比较完毕并停止,此时0~n范围内就是有序的,排序完毕
左神的抽象理解法:玩扑克牌
一副手牌按从左到右升序排列,抓到新的牌从右往左滑,滑到它前面的牌面都比它小则插入进去,再抓下一张牌继续
3.2 时间复杂度
时间复杂度为O(N^2)
可以看出常数操作的次数为一个等差数列 aN^2 + bN +C 我们只考虑最高项就是N^2
3.3 代码实现
public static void insertSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
//0~0是天然有序的
//0~1想有序
for (int i = 1; i < arr.length; i++){
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--){
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;
}
左神图解两层循环:
判断的是 0~i 位置上的数据,那么(0~i-1)范围内得数据就是之前排好的,i 相当于新来的数
令 j 取 i-1 上的数,那么当前数 i 就处在 j+1 位置上,此时进行比较若 [j]>[j+1] , 则交换
此时当前数 i 就交换到了 i-1 位置上 ,给j--则现在 j 取 i-2 上的数,而此时当前数 i 仍处于 j+1 位置上(事实是当前数永远处于 j+1 位置上),再比较若 [j]>[j+1] , 则交换,直到当前数 i 大于前一个数或者 i 到索引0位置上停止循环,此时 0~i 范围内就有序了
补充: 二分查找
1. 二分查找的三个使用策略
1.1在一个有序数组中,找某个数是否存在
(1)先找出数组的中间值mid,和目标值target进行比较
(2)若 mid > target ,说明数组后一半肯定没有目标值target,若 mid < target ,说明数组前一半肯定没有目标值target
(3)继续二分找中间值mid再和目标值target比较直到 mid=target ,此时就找到了,否则数组中没有该数据
时间复杂度:O(logN)
可以看出每一次查找都是把数组折一半,最差的情况就是折到数组仅剩1个元素,一共需要折logN次(以二为底)
如数组长度为8 那么 8-->4-->2-->1,需要3次。数组为16 那么 16-->8-->4-->2-->1,需要4次
1.2 在一个有序数组中,找>=某个数最左侧的位置
(1)先找到数组中间位置,和目标值num进行比较
(2)若中间值>=num,则说明目标值num在包括中间值的左边,此时标记中间值的索引为r,
继续在0~r上二分找中间点,若中间点< num,则说明目标值在右边,标记中间值为l,
继续在l~r上二分找中间点,若中间点 > num,则说明目标值在左边,此时把新中间点赋给r
(3)一直循环只要是中间点 > num,就把新中间点赋给r,一直缩进右边界逼近到只剩下一个值此时就找到了目标值
1.3 局部最小值问题(可以是无序数组)
局部最小的定义:
(1)两端点情况下,只要端点小于相邻数(只有一个)就是局部最小数
(2)非端点情况下,必须同时小于左右两个相邻数时才是局部最小数
思路:
(1)若两端点都不是局部最小时,此时可以模拟函数图像,两端朝中间一定都是递减的
(2)二分找到中间点,中间点左右两端至少会有一个递减的,此时两个反向递减的内部必有至少一个拐点,任意一个拐点就是局部最小值
(3)循环进行二分直到找到拐点,此时就得到了局部最小值
4. 归并排序
4.1 归并排序思想
(1)二分数组找到中间点M
(2)先让数组左侧数据(L~M)排好序,再让数组右侧数据(M~R)排好序(递归思想)
(3)最后把数组左右两侧数据合并merge起来(整体排序)
合并merge的方法
(1)先给左右两侧的半数组的首元素索引分别设为 p1、p2
(2)初始化一个辅助数组help(用来存放排序好的元素),循环比较 p1、p2 指向的元素,把较小的那个赋给辅助数组help的 i 位置上,然后 i++ ,较小元素的索引++,进行下一次循环
如上图,p1指向的元素较小,则把p1指向的元素赋给help的索引 i 上,i++,p1++,p2不变
(3)循环直到p1,p2其中一个越界(必然发生),没越界的那一侧把剩下的元素按顺序(已排序好)赋给辅助数组help
(4)最后把辅助数组的元素依次赋给主数组就完成了排序
4.2 时间复杂度
归并排序的时间复杂度为:O(N*logN)
利用master公式:T(N) = aT(N/b) + O(N^d)
master公式
看左神画的图:
T(N):代表母问题有N个数据(规模是N)
a:代表子问题被调用的次数
T(N/b):代表每一个子问题都是(N/b)规模的子问题(子问题规模是等量的)
O(N^d):代表除了子问题的调用外,剩下过程的时间复杂度
三种情况的复杂度(前人证明好的)
-----------------------------------------------------------------------------------------------------------
利用master公式:T(N) = aT(N/b) + O(N^d)
归并排序中 子问题 一次处理 一半 母问题 的数据,因此子问题规模N/2,一次递归执行两次子问题,因此a=2,剩下都是常数操作为O(N),因此d=1
此时,符合,因此时间复杂度为O(N*logN)
本质原理:
每一次进行的排序都是可重复利用的,每一次排序的结果都会作为下一次排序的部分内容再次进行排序,而且在merge方法中每一次进行比较都会确定一个元素的位置,不会造成浪费
4.3 代码实现
public static void mergeSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
process(arr, 0, arr.length -1);
}
public static void process(int[] arr, int L, int R){
if(L == R){ //此时只有一个数直接跳出
return;
}
int mid = L + ((R - L) >> 1){ //不断求中点逼近到最左端
process(arr, L, mid); //数组左半侧递归
process(arr, mid + 1, R); //数组右半侧递归
merge(arr, L, mid, R); //左右两侧合并
}
}
public static void merge(int[] arr, int L, int M, int R){
int[] help = new int[R - L +1]; //每一层递归都有一个help,长度为左右端点索引的差+1
int i = 0; //辅助数组从0开始
int p1 = L; //左半侧数组从原数组左端L开始
int p2 = M + 1; //右半侧数组从中点M+1开始
while (p1 < M && p2 <= R){//循环比较左右两侧元素,较小元素赋给i,i++,较小元素索引++
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= M){ //若p1没有越界,把剩下元素循环赋给help
help[i++] = arr[p1++];
}
while (p2 <= R){ //若p2没有越界,把剩下元素循环赋给help
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++){ //把help元素依次返回赋给arr
arr[L + i] = help[i];
}
}
代码递归思想:
(1)process方法就是让传入的数组有序,利用递归行为不断调用自身,一直二分取左半部分不断逼近直到取到左端点
(2)此时递归到底层,只剩1个数据因此L==R,跳出到递归倒数第二层执行右半侧递归(倒数第二层必定只有2个元素因此右半侧也会直接跳出),此时左右侧递归都跳出然后就可以执行merge方法了
(3)merge执行完倒数第二层递归也就顺序执行完毕,返回倒数第三层递归(倒数第三层只会有3个或4个元素),若是3个则右侧只有一个元素直接跳出,若是4个则右侧有两个元素再次进入递归直到返回到该层(倒数第三层),此时左右两侧递归都完毕(返回上层时左侧都是递归完毕的只用考虑右侧,因为每一层返回的结果都是上层的左侧)就可以执行merge方法了,继续递归直到跳出到最上层,此时左右两侧merge的结果就是整个数组了
5. 快速排序
5.1 快速排序思想
快排1.0
(1)选择数组最后一个数(索引最大)作为划分值,记为num
(2)让数组除去最后一个值的前一段区域中的值做到小于等于(<=)num的都放在num左边, >(大于)num的都放在num右边,把数组分成两部分
(3)把num和大于num区域的第一个数据做交换,此时相当于<=num区域扩充1个位置并且最后一个数一定是num(num也是最大的),剩下的都是>区域的,此时认为num这个位置就排好了
(4)num位置已经固定,<=num区域取最后一个位置作为划分值,>num区域取最后一个值作为划分值,不断递归这个过程,每一次递归都会确定一个位置,所以递归到最后就是有序的了
例子:
划分值num为5,>5区域的第一个值设为6,5和6交换就可以确定出num的位置,左右两侧重复递归
时间复杂度
快排1.0时间复杂度为:O(N^2)
最差的情况下是每一次的划分值都是当前数组最大值,划分后只有左侧数据没有右侧数据,相当于每一次都处理(n - i)个数据
可以看出常数操作的次数为一个等差数列 aN^2 + bN +C 我们只考虑最高项就是N^2
最好情况是划分值打到几乎中间的位置
使用master公式可以得出最佳时间复杂度是O(N*logN)
快排2.0
快排2.0比快排1.0稍快一些
在快排1.0的基础上进行改进:
(1)把数组分成三个部分,即左侧放<num的区域,中间放=num的区域,右侧放>num的区域
(2)>num区域的最后一个值作为划分值,把num和大于num区域的第一个数据做交换,这样等于num的数据就都靠在一起了,等于num的区域就固定了,相当于搞定了一批数据。
例子:
时间复杂度
和快排1.0是一样的
快排2.0时间复杂度为:O(N^2)
最差的情况下是每一次的划分值都是当前数组最大值,划分后只有左侧数据没有右侧数据,相当于每一次都处理(n - i)个数据
可以看出常数操作的次数为一个等差数列 aN^2 + bN +C 我们只考虑最高项就是N^2
最好情况是划分值打到几乎中间的位置
使用master公式可以得出最佳时间复杂度是O(N*logN)
快排3.0
在快排1.0的基础上进行优化:
在数组中随机取一个数和最后一个值交换,然后拿它作为划分值
5.2 时间复杂度(快排3.0)
快排3.0的时间复杂度为:O(N*logN)
因为选取每一个位置都是等概率事件,所以每一个master公式出现也是等概率事件(权重1/N),
把所有mastr公式求概率累加再求数学上的长期期望得出时间复杂度为O(N*logN)
空间复杂度:
空间复杂度为O(N)
最差情况下一共开辟了n层递归区域
好的情况下就是每次划分值在中间,为O(logN)
此时递归类似完全二叉树展开
5.3 代码实现(快排3.0)
public static void quickSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
quickSort(arr, 0, arr.length -1);
}
public static void quickSort(int[] arr, int L, int R){
if(L < R){
swap(arr, L + (int)(Math.random() * (R - L +1)), R);//在数组中等概率选一个数,math.random左闭右开,所以R-L+1
int[] p = partition(arr, L ,R);//此时最后一个数R就是选出来的数作为划分值,返回值
//为划分值==区域的左边界和右边界(一定是长度为2的
//数组)
quickSort(arr, L, p[0] - 1);//<划分值区域上的最后一个数
quickSort(arr, p[1] + 1, R);//>划分值区域上的第一个数
}
}
//这是一个处理arr[l..r]的函数
//默认以arr[r]做划分,arr[r] -> p <p ==p >p
//返回等于区域(左边界,右边界),所以返回一个长度为2的数组res,res[0] res[1]
public static int[] partition(int[] arr, int L, int R){
int less = L - 1; // <划分值区间的右边界,从i的前一个数开始向右侧逼近
int more = R; // >划分值区间的左边界,从r开始(考虑的是除去最后一个值的前一个区域中的值)向左逼近
while(L < more){ // L=more时当前数和>划分值区间的左边界碰到,此时L右侧就都是>划分值的数了
if(arr[L] < arr[R]){ // 当前数 < 划分值
swap(arr, ++less, L++); //当前数和右边界后一个位置交换,并且L++
}else if (arr[L] > arr[R]) { // 当前数 > 划分值
swap(arr, --more, L); //当前数和左边界前一个位置交换,此时L不变
}else {
L++; //当前数 = 划分值,直接跳过,L++
}
}
// 循环完毕后已经把划分值前面的数据按三层(小等大)排好了,此时more指向>划分值区间的第一个数
// 交换more和R(R指向划分值),这时包括划分值在内的整个数组就排序完毕了,more指向的就是划分值==区间最后一个数
swap(arr, more, R);
return new int[]{less + 1,more};//less+1就是左边界前一个位置,也就是==区间第一个数(左边界)
//more此时指向==区间最后一个数(more的值和R交换了)
}
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
递归思想:
(1)每一层递归执行一次划分区间,每执行一次划分区间都会返回当前层的划分值==区间左右边界的索引,
(2)然后不断递归向左逼近直到底层递归的数组长度为1(此时L=R),不符合if条件,跳出到递归倒数第二层执行划分值右侧区域的递归
(3)每一层都是排好序再返回到上层递归,最后到顶层递归的时候数组也就排序好了
6. 堆排序
6.1 堆排序的思想
6.1.1 数组和完全二叉树的关系
堆排序是建立在数组转化成完全二叉树结构思想的基础之上的一种排序
把要排序的数组长度赋给size记录,然后把这个数组脑补成完全二叉树的结构,此时根据完全二叉树的性质就可以算出数组中每个元素的相对位置
i 的左子树 2 * i + 1 i 的右子树 2 * i + 2 i 的父节点 (i - 1)/2
6.1.2 两种堆结构
分为大根堆和小根堆
顾名思义:大根堆就是在一个完全二叉树中每一颗子树的最大值就是头节点的值
小根堆就是在一个完全二叉树中每一颗子树的最小值就是头节点的值
6.1.3 堆排序的基本思路
(1)把传入的数组整体转化成大根堆的结构
过程:①先设大根堆对应数组长度heapSize=0
②取数组第一个值放在索引0上并看作是根节点,heapSize++
③不断取数组元素依次放在左右孩子节点,然后依次和父节点进行比较,孩子节点小 则直接取下一个,孩子节点大则和父节点交换,与父节点交换后,相应的数组上位 置也交换
交换后:
④这个过程就叫做heapInsert过程,多次heapInsert后就可以成为大根堆,如下图:
(2)把最大值(索引肯定为0)和最后一个值进行交换,
然后heapSize--(等同于从堆上拿掉了最后一个值,最后一个值和堆断开连接)
这时就固定了断开连接的值也就是最后一个值
交换后 heapSize--
(3)从0位置上作heapify让剩下的堆再次变成大根堆
heapify过程:①把位置为0的节点和比它大的孩子节点交换,直到换到没有孩子节点
交换后以上是关于数据结构十大排序算法+二分查找(左程云 左神版 全文2W字+ word很大,你忍一下~~)的主要内容,如果未能解决你的问题,请参考以下文章
百度云好课分享[左程云_算法与数据结构进阶班马士兵教育更新完] 百度网盘