目录
目录
一、冒泡排序
1.方法步骤:
对长度为 n 的数组arr进行多轮排序,排序过程中,
(1)设置一个指针p指向arr[0],即p=0
(2)将p位置与p+1位置的数进行比较,如果arr[p] > arr[p+1],交换arr[p]与arr[p+1]所指向的数,否则不操作;
(3)p向后移一位,每轮排序结束后可以保证参与当前轮排序最大的数会被放在最后,
(3)在下一轮的排序中,对未排好的数重复进行上述排序操作,直到整个数组有序为止
如何判断当前轮排完,数组有序了呢?可以设置一个初始值为false的布尔型变量,来记录本轮排序是否存在交换操作,如果存在交换操作,则设置成true,本轮排序结束后,如果该变量的值为false,表示本轮没有交换操作,说明数组已经有序,反之无序
举例:
给定一个数组 arr = [ 3, 1, 5, 9, 6, 8, 0, 3 ],对这个数组进行排序
位置 0, 1, 2, 3, 4, 5, 6, 7
[ 1, 3, 5, 6, 8, 0, 3, 9 ] 第一轮排位置0-7,排序后,最大值9已经被放到位置7
[ 1, 3, 5, 6, 0, 3, 8, 9 ] 第二轮排位置0-6,排序后,最大值8已经被放到位置6
[ 1, 3, 5, 0, 3, 6, 8, 9 ] 第三轮排位置0-5,排序后,最大值6已经被放到位置5
[ 1, 3, 0, 3, 5, 6, 8, 9 ] 第四轮排位置0-4,排序后,最大值5已经被放到位置4
[ 1, 0, 3, 3, 5, 6, 8, 9 ] 第五轮排位置0-3,排序后,最大值3已经被放到位置3
[ 0, 1, 3, 3, 5, 6, 8, 9 ] 第六轮排位置0-2,排序后,最大值3已经被放到位置2
此时,经过六轮排序,数组已经有序
2.稳定性
稳定的
在上面的例子的第五轮排序中,当第一个3与第二个3相比较的时候,由于不满足arr[p] > arr[p+1],因此不交换,所以排序结束时,两个3的相对位置并没有改变
3.时间复杂度
(1)最好情况:O(N)
数组已经有序,如:array = [0, 1, 2, 3, 4, 5],还需进行一轮的比较即可判定数组有序,需要比较(n-1)也就是5次,因此时间复杂度为O(N)
(2)最坏情况:O(N^2)
数组逆序,如:array = [5, 4, 3, 2, 1],那么就一共需要进行(n-1)轮排序,每轮需要进行(n-1)次比较,一共进行的比较次数为(n-1) + (n-2) + ... + 2 + 1 = n(n-1)/2,取最高次项并去掉系数得时间复杂度为O(N^2)
一般来说,我们所说的时间复杂度指的是最坏的情况的时间复杂度
4.空间复杂度
O(1)
排序的过程中只定义了一个指针p,没有再额外申请空间
二、选择排序
1.方法步骤
对长度为 n 的数组arr进行多轮排序,排序过程中,
(1)设置一个指针p指向arr[0],即p=0
(2)取p到n-1位置中的最小值arr[m],并交换arr[m]与arr[p]的值
(3)此时(0-p)位有序,p位置为参与排序部分的最小值,p向右移一位
(4)重复(2)(3)步骤的操作,直到p=n-1,此时数组有序
举例:
给定一个数组 arr = [ 3, 1, 9, 3, 6, 8, 0, 5 ],对这个数组进行排序
位置 0, 1, 2, 3, 4, 5, 6, 7
[ 0, 1, 9, 3, 6, 8, 3, 5 ] 第一轮排位置0-7,将最小值arr[6]=0与arr[0]=3换位置
[ 0, 1, 9, 3, 6, 8, 3, 5 ] 第二轮排位置1-7,将最小值arr[1]=1与arr[1]=1换位置,0-1有序
[ 0, 1, 3, 9, 6, 8, 3, 5 ] 第三轮排位置2-7,将最小值arr[3]=3与arr[2]=9换位置,0-2有序
[ 0, 1, 3, 3, 6, 8, 9, 5 ] 第四轮排位置3-7,将最小值arr[6]=3与arr[3]=9换位置,0-3有序
[ 0, 1, 3, 3, 5, 8, 9, 6 ] 第五轮排位置4-7,将最小值arr[7]=5与arr[4]=6换位置,0-4有序
[ 0, 1, 3, 3, 5, 6, 9, 8 ] 第六轮排位置5-7,将最小值arr[7]=6与arr[5]=8换位置,0-5有序
[ 0, 1, 3, 3, 5, 6, 8, 9 ] 第七轮排位置6-7,将最小值arr[7]=8与arr[6]=9换位置,0-6有序
此时p向后移一位后,p = n-1,停止排序,数组有序
2.稳定性
不稳定
上述例子中,在第一轮排序交换后,第一个3已经跑到了第二个3的后面,排序完成后,两个3的相对位置发生了改变,因此是不稳定的
3.时间复杂度
最好情况和最坏情况都为O(N^2)
因为无论是什么样的数组,每轮排序都会遍历整个参加排序的部分来寻找最小值,因此必定需要排(n-1)轮,需要查看的总次数为 n + (n-1) + ... + 2 + 1 = n(n+1)/2,可得到时间复杂度为O(N^2)
4.空间复杂度
O(1)
排序的过程中只定义了一个指针p,没有再额外申请空间
三、插入排序
1.方法步骤
对长度为 n 的数组arr进行多轮排序,排序过程中,
(1)设置一个指针p指向arr[1]
(2)将arr[p]的值依次向前比较,当arr[p]的值小于位置m的数的值时,将arr[p]的值插入到m位置,如果arr[p] >= arr[p-1],表示p位置已是最大值,不需移动,直接跳到(3),否则原本m到(p-1)的数依次向后移一位
(3)此时(0-p)位置有序,指针p = p + 1,重复(2)操作
(4)重复(2)(3)操作,直到(p == n),此时数组有序
举例:
给定一个数组 arr = [ 3, 2, 1, 9, 3],对这个数组进行排序
位置 0, 1, 2, 3, 4, 5, 6, 7
[ 2, 3, 1, 9, 3 ] 第一轮p指向arr[1]=2,插入后,3后移,此时0-1有序
[ 1, 2, 3, 9, 3 ] 第二轮p指向arr[2]=1,插入后,2,3后移,此时0-2有序
[ 1, 2, 3, 9, 3 ] 第三轮p指向arr[3]=9,9最大,不需插入,此时0-3有序
[ 1, 2, 3, 3, 9 ] 第四轮p指向arr[4]=3,插入后,9后移,此时0-4有序
此时数组有序
2.稳定性
稳定的
第四轮排序时,当第一个3与第二个3比较时,由于不满足arr[p] < arr[m],因此没有改变两个3的相对位置
3.时间复杂度
(1)最好情况O(N)
当数组有序时,每轮排序只需要比较p位置和p-1位置,即每轮只比较一次,所以时间复杂度是O(N)
(2)最坏情况O(N^2)
数组逆序,每轮排序都需要遍历参与排序的所有的元素,总的比较次数为1 + 2 + ... + (n-1) = n^2/2,因此时间复杂度为O(N^2)
4.空间复杂度
O(1)
排序的过程中只定义了一个指针p,没有再额外申请空间
四、归并排序
1.方法步骤
归并排序本质上是一个把大问题细化成小问题的过程,即递归过程,利用了分治的原则
假设一个方法Func为递归排序方法,需要传入一个待排序数组arr,这个方法的内部功能为:
(1)将这个数组分为两部分(一般来说第一部分的长度为数组长度lenth / 2)
(2)对分开的左右数组再次分别调用Func方法,可以得到有序的的左数组和右数组
(3)设指针PL指向左数组的第一个元素,PR指向右数组的第一个元素,和一个和原数组长度相等的空数组arr1
(4)比较PL与PR指向的值,如果arr[PL] <= arr[PR],将arr[PL]放入数组arr1,PL右移,否则将arr[PR]放入数组arr1,PR右移
(5)重复操作(4),直到PL或者PR到达数组末尾,然后把另一个数组后面的所有元素依次放入数组arr1中,并返回arr1
此时arr1输一个有序的数组
在Func的开头,需要判定一下,如果传进来的数组长度为1,则无需排序,直接返回该数组
举例:
给定一个数组 arr = [ 3, 0, 1, 3, 9, 7 ],长度为6,对这个数组进行排序
2.稳定性
稳定的
在归并阶段,满足arr[PL] <= arr[PR],就会优先取左数组的数,在上例中,当两个3作比较时,会先取左数组的3,再取右数组的3,所以两个3的相对位置没有发生改变
3.时间复杂度
最好和最坏情况都为O(NlogN)
归并排序拆分的过程最终是要把原数组拆分成只有一个元素的子数组,
第一层子数组长度为N/2
第二层子数组长度为N/4
。。。
第logN层子数组的长度为1
在合并的过程中,每一层都需要遍历一遍原数组来比较,遍历的时间复杂度为O(N),一共有logN层,因此时间复杂度为O(NlogN)
4.空间复杂度
O(N)
排序过程中申请了一个与原数组长度相等的临时数组arr1
五、快速排序
1.方法步骤
快速排序也利用了递归的过程
给定一个长度为n数组arr进行排序
(1)在数组arr中选择一个基准值m,基准值的选择方式有:
1> 选取数组的最后一个数或中位数
2>随机选择数组中的一个数
(2)将数组中小于m的数放在左侧,大于m的数放在右侧,等于m的数放在中间,这个操作叫做partition
(3)分别对数组中小于m部分和大于m部分的子数组进行(1)(2)操作
最终返回的数组就是有序的
对于操作(2)的具体实现:
将基准值m与数组最后一个元素交换位置,设指针pl为左边界初始值为-1,指针pr为右边界初始值为n,左边界的左边为小于等于m的元素,右边界的右边为大于m的元素,左边界和右边界之间为等于m的元素,通过索引 i 遍历整个数组
如果arr[i] < 基准,将arr[i]与左边界后一个数换位置,左边界右移一位;
如果arr[i] > 基准,将arr[i]与右边界前一个数换位置,右边界左移一位;
如果arr[i] == 基准,左边界右移一位
当左边界与右边界相遇是,停止遍历
2.稳定性
不稳定
3.时间复杂度
(1)选取数组的最后一个数或中位数作为基准 O(N^2)
最差情况,最后一个数作为基准,当数组是顺序或者逆序的时候,如[1, 2, 3, 4, 5],需要经历n层递归,每层递归都需要遍历一下参与排序的数,由等差数列可得时间复杂度为 O(N^2)
最好情况,中位数作为基准,数组基本有序,需要经历logn层递归,每层递归都需要遍历一下参与排序的数,时间复杂度为 O(NlogN)
(2)随机选择数组中的一个数O(NlogN)
随机选择基准,就变成了一个概率问题,这里不再详细证明
4.空间复杂度
O(logN)
六、堆排序
1.方法步骤
概念:大根堆
一棵二叉树中每一个节点都满足大于等于其左右节点的值
堆排序分为两个步骤:heapInsert 和 heapify
步骤1 heapInsert——构造大根堆
每插入一个数,都要与其父节点相比较,如果大于父节点,则交换位置,交换后再与其父节点比较,直到小于等于父节点或其已经为根节点为止
那么已知当前节点的索引 i ,如何去找其父节点的索引 f 呢?
用公式:f = (i-1) / 2
特别的如果当前节点为根节点,索引为0,利用上述公式求得的值还是为0
举例:
对数组arr = [ 5, 3, 2, 6, 7, 2, 0 ],构造大根堆
[ 5, 3, 2, 6, 7, 2, 0 ]
索引 0, 1, 2, 3, 4, 5, 6
首先插入arr[0] = 5,根据公式可得,其父节点的索引还是0,因此为根节点
插入arr[1] = 3, 其父节点索引为0,且arr[1] < arr[0],直接插入即可
插入arr[2] = 2, 其父节点索引为0,且arr[2] < arr[0],直接插入即可
插入arr[3] = 6, 其父节点索引为1,且arr[3] > arr[1],将arr[3] = 6与arr[1] = 3交换,此时arr[3] = 3,arr[1] = 6;
又因此时arr[1] = 6大于其父节点arr[0] = 5,将arr[1]与arr[0]进行交换,此时arr[1] = 5,arr[0] = 6;
arr[0]已无父节点,无需操作
插入arr[4] = 7, 其父节点索引为1,且arr[4] > arr[1],将arr[4] = 7与arr[1] = 5交换,此时arr[4] = 5,arr[1] = 7;
又因此时arr[1] = 7大于其父节点arr[0] = 6,将arr[1]与arr[0]进行交换,此时arr[1] = 6,arr[0] = 7;
arr[0]已无父节点,无需操作
插入arr[5] = 2, 其父节点索引为2,且arr[2] == arr[5],直接插入即可
插入arr[6] = 0, 其父节点索引为2,且arr[2] > arr[6],直接插入即可
大根堆构造完毕,层序遍历大根堆得到当前数组为 arr = [ 7, 6, 2, 3, 5, 0, 2 ]
步骤2 heapify——弹出与调整
经过步骤1,当前堆得根节点为数组的最大值,步骤2就是将根节点弹出,然后再将这可二叉树恢复成大根堆,重复操作直到所有的数都弹出为止
如何弹出呢?就是将根节点与最后一个叶节点换位置即可,对于上述例子,就是交换arr[0] = 7与arr[6] = 2
此时 arr = [ 2, 6, 2, 3, 5, 0, 7 ]
交换后,再对剩下的arr[0]~arr[5]调整为大根堆(调整过程不再详述,与步骤1类似),构造完成后交换arr[0]与arr[5]
此时 arr = [ 0, 5, 2, 2, 3, 6, 7 ]
交换后,再对剩下的arr[0]~arr[4]调整为大根堆,构造完成后交换arr[0]与arr[4]
此时 arr = [ 2, 2, 3, 0, 5, 6, 7 ]
交换后,再对剩下的arr[0]~arr[4]调整为大根堆,构造完成后交换arr[0]与arr[4]
。。。
重复以上步骤,直到所有数都弹出,可得到有序数组arr = [ 0, 2, 2, 3, 5, 6, 7 ]
2.稳定性
不稳定
(发现一个规律,只要排序算法中交换的距离>1的都可能会导致算法不稳定)
3.时间复杂度
O(NlogN)
4.空间复杂度
O(1)
未完待续