这是我在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的节点和比它大的孩子节点交换,直到换到没有孩子节点
交换后 再换
(4)再次把最大值和最后一个值进行交换,然后heapSize--, 这时就再次固定了断开连接的值也就是最后一个值
(5)一直这样循环,会把所有的元素放在正确的位置上,当堆得大小减成0的时候就排好序了
6.2 时间复杂度
堆排序的时间复杂度为O(N*logN)
(1)建立大根堆O(N*logN),本质上就是log(1)+log(2)+…..+log(N)
还有个更快一点的办法
第二种方法
数组转化成的完全二叉树从后往前(从右往左)一直做heapify就可以变成大根堆
这时复杂度为:O(N)
此时常数项操作为一个等比数列
(2)接下来的Heapify也是一样为O(N*logN)
(3)省略常数项总体还是O(N*logN)
空间复杂度
空间复杂度为:O(1)
只有在heapify里申请了几个变量,为常数值复杂度
6.3 代码实现
左神的代码为:
public static void heapSort(int arr[]){
if(arr == null || arr.length < 2){
return;
}
for(int i = 0; i < arr.length; i++){ //把整个数组先转化成大根堆的形式
heapInsert(arr,i);
}
int heapSize = arr.length; // 确定堆的长度
swap(arr, 0, --heapSize); //0位置上的数和最后一个位置的数交换,堆的大小--(固定最后一个值)
while (heapSize > 0){ // 堆的大小没减到0就一直循环
heapify(arr, 0, heapSize); //0位置的数往下heapify转成大根堆
swap(arr, 0, --heapSize); //再次交换0位置上的数和最后一个位置的数,堆的大小--
}
}
public static void heapInsert(int[] arr, int index) { //传入元素的索引
while (arr[index] > arr[(index - 1)/2]) { //如果孩子节点比父节点大
swap(arr, index, (index - 1)/2); //交换孩子节点与父节点的位置
index = (index - 1)/2; //交换后的索引位置赋给index供继续循环使用
}
}
public static void heapify(int[] arr, int index, int heapSize){
//其中index 决定从哪个节点作根节点开始往下heapify, Heapsize 决定堆对应的数组长度判断孩子节点是否越界
int left = index * 2 + 1; //左孩子的下标
while (left < heapSize){ //左孩子的下标没越界代表下方有孩子节点
int largest = left + 1 < heapSize && arr[left + 1] > arr[left]
? left + 1 : left; // 两个孩子中,值较大的把下标赋给largest
largest = arr[largest] > arr[index] ? largest : index; //父和孩子之间较大值下标给largest
if(largest == index){ //代表根节点最大,直接跳出
break;
}
swap(arr, largest, index); //代表孩子节点大,和父节点交换值
index = largest; //把孩子节点的下标赋给index,相当于index指向下一层继续循环
left = index * 2 + 1; //新的左孩子下标依旧为当前index * 2 + 1
}
}
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
代码思想:
(1)先把数组整个转化成为大根堆的形式,并确定堆的长度
(2)此时大根堆的0索引指向的元素一定是最大的,把它和最后一个位置的元素交换,就固定了最后一个位置的元素,把helpSize--(接下来就不用考虑堆的最后一个元素,所以把它断开)
(3)交换后新的堆就不是大根堆的形式了,这时利用heapify把新的堆再次转化成大根堆
(4)再把大根堆的0索引指向的元素和最后一个位置的元素交换,就固定了最后一个位置的元素,再把helpSize--
(5)一直重复(3)和(4)步骤直到堆的长度减到0,此时堆就只剩原数组0索引的元素了,数组也就排序好了
7. 希尔排序
7.1 希尔排序的基本思想
希尔排序就是为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序
(1)希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 i 的元素有序
(2)刚开始 i 的大小可以是 i = n / 2,接着让 i = n / 4,让 i 一直缩小
(3)当 i = 1 时,也就是此时数组中任意间隔为 1 的元素有序,此时的数组就是有序的了
7.2 时间复杂度
希尔排序的时间复杂度为:O(N*logN)
空间复杂度:O(1)
希尔排序的复杂度和增量序列是相关的,希尔排序的时间度分析极其复杂,有的增量序列的复杂度至今还没人能够证明出来
7.3 代码实现
public class ShellSort {
public static int[] shellSort(int arr[]) {
if (arr == null || arr.length < 2) return arr;
int n = arr.length;
// 对每组间隔为 h的分组进行排序,刚开始 h = n / 2;
for (int h = n / 2; h > 0; h /= 2) {
//对各个局部分组进行插入排序
for (int i = h; i < n; i++) {
// 将arr[i] 插入到所在分组的正确位置上
insertI(arr, h, i);
}
}
return arr;
}
/**
* 将arr[i]插入到所在分组的正确位置上
* arr[i]] 所在的分组为 ... arr[i-2*h],arr[i-h], arr[i+h] ...
*/
private static void insertI(int[] arr, int h, int i) {
int temp = arr[i];
int k;
for (k = i - h; k > 0 && temp < arr[k]; k -= h) {
arr[k + h] = arr[k];
}
arr[k + h] = temp;
}
}
8. 桶排序
8.1 桶排序基本思想
桶排序就是把最大值和最小值之间的数进行瓜分
(1)把数组分成 i 个区间,i 个区间对应 i 个桶,我们把各元素放到对应区间的桶中去,再对每个桶中的数进行排序,可以采用归并排序,也可以采用快速排序之类的
(2)之后每个桶里面的数据就是有序的了,我们再进行合并汇总
8.2 时间复杂度
桶排序的时间复杂度为:O(N+K)
空间复杂度:O(N+K)
注:K表示桶的个数
8.3 代码实现
public class BucketSort {
public static int[] BucketSort(int[] arr) {
if(arr == null || arr.length < 2) return arr;
int n = arr.length;
int max = arr[0];
int min = arr[0];
// 寻找数组的最大值与最小值
for (int i = 1; i < n; i++) {
if(min > arr[i])
min = arr[i];
if(max < arr[i])
max = arr[i];
}
//和优化版本的计数排序一样,弄一个大小为 min 的偏移值
int d = max - min;
//创建 d / 5 + 1 个桶,第 i 桶存放 5*i ~ 5*i+5-1范围的数
int bucketNum = d / 5 + 1;
ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(bucketNum);
//初始化桶
for (int i = 0; i < bucketNum; i++) {
bucketList.add(new LinkedList<Integer>());
}
//遍历原数组,将每个元素放入桶中
for (int i = 0; i < n; i++) {
bucketList.get((arr[i]-min)/d).add(arr[i] - min);
}
//对桶内的元素进行排序,我这里采用系统自带的排序工具
for (int i = 0; i < bucketNum; i++) {
Collections.sort(bucketList.get(i));
}
//把每个桶排序好的数据进行合并汇总放回原数组
int k = 0;
for (int i = 0; i < bucketNum; i++) {
for (Integer t : bucketList.get(i)) {
arr[k++] = t + min;
}
}
return arr;
}
}
9. 计数排序
9.1 计数排序基本思想
利用词频表
(1) 统计出需要排序的范围,创建一个词频数组来存储每一个元素出现的次数
(2)把词频数组还原成原数组就排好序了
9.2 时间复杂度
计数排序的时间复杂度为:O(N + K)
额外空间复杂度为:O(K)
K表示临时数组的大小
9.3 代码实现
public class Counting {
public static int[] sort(int[] arr) {
if(arr == null || arr.length < 2) return arr;
int n = arr.length;
int min = arr[0];
int max = arr[0];
// 寻找数组的最大值与最小值
for (int i = 1; i < n; i++) {
if(max < arr[i])
max = arr[i];
if(min > arr[i])
min = arr[i];
}
int d = max - min + 1;
//创建大小为max的临时数组
int[] temp = new int[d];
//统计元素i出现的次数
for (int i = 0; i < n; i++) {
temp[arr[i] - min]++;
}
int k = 0;
//把临时数组统计好的数据汇总到原数组
for (int i = 0; i < d; i++) {
for (int j = temp[i]; j > 0; j--) {
arr[k++] = i + min;
}
}
return arr;
}
}
10.基数排序
10.1 基数排序思想
注:选桶时数字是几进制就选几个桶
(1)先看一下最大的数字有几位,设为 i ,然后把长度不到 i 位的补0一直补到也是长度为 i 位
(2)选取十个“桶”(容器),这里的容器采用队列,取值 0~9
(3)从左往右把数据放进桶里,先根据个位数字决定数据进入那个桶里,个位是几就放到几号桶里
(4)把桶里的数据从左往右依次倒出来 (队列:先进先出)
(5)再根据十位数字决定数据进入那个桶里,从左往右把数据放进桶里,再把桶里的数据从左往右依次倒出来
倒出
(6)再循环取更高一位数字进桶出桶,直到取到所有数据的最高位
10.2 时间复杂度
基数排序的时间复杂度为:O(N)
额外空间复杂度为:O(N)
10.3 代码实现
public static void RadixSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
radixSort(arr, 0, arr.length - 1, maxbits(arr));
}
public static int maxbits(int[] arr){
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
}
int res = 0;
while (max != 0){
res++;
max /= 10;
}
return res;
}
public static void radixSort(int[] arr, int L, int R, int digit){
final int radix = 10;
int i = 0, j = 0;
int[] bucket = new int[R - L + 1];
for (int d = 1; d <= digit; d++){
int[] count = new int[radix];
for (i = L; i <= R; i++){
j = getDiggit(arr[i], d);
count[j]++;
}
for (i = 1; i < radix; i++){
count[i] = count[i] + count[i - 1];
}
for (i = R; i >= L; i--){
j = getDiggit(arr[i], d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
for (i = L, j = 0; i <= R; i++,j++){
arr[i] = bucket[j];
}
}
}
public static int getDiggit(int x, int d){
return ((x / ((int)Math.pow(10, d - 1))) % 10);
}
11. 排序算法的稳定性与总结
11.1 稳定性的定义
同样值的个体之间,如果不因为排序而改变相对次序,就是这个排序是有稳定
性的,否则就没有
图示:
11.2 不具备稳定性的排序
不具备稳定性的排序有4种:选择排序、快速排序、堆排序、希尔排序
①选择排序:3和1交换后就已经改变了相对次序,做不到稳定性了
②快速排序:在partition的时候已经做不到稳定性了
此时3和大于(<划分值区间)的第一个6交换,改变了相对次序
和
③堆排序:在数组转化成大根堆的时候已经做不到稳定性了
④希尔排序
虽然插入排序是稳定的,但是希尔排序在插入的时候是跳跃性插入的,有可能破坏稳定性
11.3 具备稳定性的排序
具备稳定性的排序:冒泡排序、插入排序、归并排序、一切桶排序思想下的排序
①冒泡排序:比较时遇到相等的值不交换,相对次序可以保证不变
②插入排序:比较时遇到相等的值也不交换,相对次序可以保证不变
遇相等
③归并排序:merge的时候遇到相等的永远先考虑左边的,相对次序可以保证不变
左边完了在拷贝右边
11 .4 六大经典排序算法总结
左神纯手画的图:
一般情况下用快排(实用性高)
对空间有较高要求用堆排
对稳定性有要求用归并排序
目前没有找到时间复杂度O(N*logN),额外空间复杂度O(1),又稳定的排序。
11.5 常见的坑
1. 归并排序的额外空间复杂度可以变成O(1),但是非常难,不需要掌握,有兴趣可以搜“归并排序内部缓存法”
2. “原地归并排序”的帖子都是垃圾,会让归并排序的时间复杂度变成O(N^2)
3. 快速排序可以做到稳定性问题,但是非常难,不需要掌握,可以搜 “01stable sort”
4.所有的改进都不重要,因为目前没有找到时间复杂度O (N*logN),额外空间复杂度O(1),又稳定的排序
5.有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,碰到这个问题,可以怼面试官,基本只在论文级别中出现
12. 个人感悟
总结完了这十大排序让我对于数据结构和算法真的是有了更加全面和深层次的认识,我深知算法是我目前需要加强的,做了些leetcode题感觉有点吃力。。。所以还是打算先把基础好好总结一遍,这次就花了大概整整四天的时间好好的把十大排序,其实主要还是六大经典排序给总结了一遍,然后写了个博客记录总结,希望接下来做LeetCode题能比以前轻松一点吧。
这三天的日子确实有点难熬(因为数据结构比较薄弱理解起来还是挺吃力的,其中归并、快排、堆排每一个都花了近乎大半天时间)。但是又感觉过的其实又快,坐在电脑前刚理解了一个排序才发现原来时间都过去三几个小时了。
其中不乏有很多时间段让我感到迷茫,感到意志消沉。不过总的来说还是很庆幸我能振作自己然后把这件事坚持了下来,从简单的开始,我相信会克服困难的,让自己的实力得到更好的提升,让自己距离理想中的那个自己更近一步。
看到这句话的小伙伴们希望你们继续振作自己,大家一起努力,一起成为想要成为的那个人。
希望大家能喜欢这篇总结
感谢大家的阅读(*^▽^*) ٩(๑>◡<๑)۶
其中可能会有一些错误或者纰漏,还请大家提醒我改正,谢谢我的小伙伴们
这次挑选了一张比较有意义的图来纪念这个有意义的博客(是我最喜欢的皮肤啦~~)
最后美图收尾嘻嘻~~(KDA POPSTAR 卡莎 至臻 2018)