03 排序
冒泡排序
冒泡排序思路
- 有 n 个数,需要 i=n-1 趟排序,每趟需要比较 i 次
- 每趟前面的数和后面的数做比较,前面的数如果比后面的大,那么两两进行交换,大的数冒泡到后面去
- 每趟将最大的值冒泡到最后一个位置去
举例子
以 3412 为例,需要 n-1=3 趟冒泡 第 1 趟 (0
n-1 03):3124,将 4 冒泡到这趟的最后 第 2 趟 (0n-2 02),1234,将 3 冒泡到这趟的最后 第 3 趟 (0~n-1 0-1),1234,将 2 冒泡到这趟的最后
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 以3412为例
// 0~N-1 0~3
// 0~N-2 0~2
// 0~N-3 0~1
int N = arr.length;
// 由于每趟排序后最后一个元素为最大值,即下趟排序后不参与了,所以控制趟数的end用倒序来控制
for (int end = N - 1; end >= 0; end--) {
for (int j = 0; j < end; j++) { // 每趟两个数两两进行比较,前面大的数交换到后面去
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
}
选择排序
选择排序思路
- 有 n 个数,选择排序需要 i=n-1 趟选择
- 选择排序每次选择一个最小的值,放在最前面
- 选择的过程中,默认第 0 个位置的元素为最小值,只更新最小值的索引,在每趟结束后,将最小的值和每趟的第 0 个元素交换位置
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int N = arr.length;
for (int i = 0; i < N - 1; i++) { // 控制趟数
int minValueIndex = i; // 记录最小值的索引
for (int j = i + 1; j < N; j++) { // 每趟,默认第0个元素为最小值
if (arr[j] < arr[minValueIndex]) {
minValueIndex = j;
}
}
swap(arr, i, minValueIndex);
}
}
插入排序
排入排序思路
类比抓扑克牌,先来一张牌,将该牌插入到前面已经排好序的牌中去
- n 个数,需要 n-1 趟插入排序
- 默认每趟的第 0 个元素已经排好序了
- 每次往后取下一个元素和前面已经排好序的进行比较,如果比前面的小,交换位置;继续往前走,直到遇到比其大的值或者到边界了
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int N = arr.length;
for (int i = 0; i < N - 1; i++) { // 控制躺数
for (int j = i + 1; j > 0; j--) { // 从排好序的下一个,倒序的
if (arr[j] < arr[j - 1]) { // 如果后面的元素比前面的小,交换位置,继续往前走
swap(arr, j, j - 1);
} else { // 只要后面的比前面的小,终止循环,因为前面的已经是有序的,没必要再做笔记,因为都会比他小
break;
}
}
}
}
归并排序 (大厂常考)
堆排序 (大厂常考)
思路 (升序为例):将数组构建为一个大顶堆,首元素即为数组最大值,首尾元素交换;排除末尾元素后调整大顶堆,则新的首元素即为次最大值,交换首尾并再排除末尾元素;如此循环,最后的数组即为升序排列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class HeapSort02 {
public static void main(String []args){
int []arr = {2,1,8,6,4,7,3,0,9,5};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
int len = arr.length;
// 创建一个大顶堆
for(int i = (int) Math.ceil(len/2 - 1); i >= 0; i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr,i,len);
}
// 交换首尾元素,并重新调整大顶堆
for(int j = len-1;j > 0;j--){
swap(arr,0,j);
adjustHeap(arr,0,j);
}
}
/** 迭代写法*/
public static void adjustHeap(int []arr,int i,int length){
int temp = arr[i];
for (int k = 2*i + 1; k < length; k=k*2 + 1) {
// 注意这里的k + 1 < length
// 如果右子节点大于左子节点,则比较对象为右子节点
if (k + 1 < length && arr[k] < arr[k+1]){
k++;
}
if (arr[k] > temp){
// 不进行值交换
arr[i] = arr[k];
i = k;
}
else{
break;
}
}
arr[i] = temp;
}
/** 递归写法*/
private static void adjustHeap2(int[] arr, int i, int len){
int left = 2 * i + 1;
int right = 2 * i + 2;
int maxIndex = i;
// 注意这里的 left < len
if (left < len && arr[left] > arr[maxIndex]){
maxIndex = left;
}
if (right < len && arr[right] > arr[maxIndex]){
maxIndex = right;
}
if (maxIndex != i){
swap(arr,i,maxIndex);
adjustHeap2(arr,maxIndex,len);
}
}
/** 交换元素 */
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
快速排序 (大厂常考)
全名 K 歌二面
快速排序小结
一句话总结快速排序:快速排序是先将一个元素排好序,然后再将剩下的元素排好序。
什么是快速排序?
快速排序是对冒泡排序的一种改进。快速排序由 C.A.R. Hoare 在 1960 年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序原理
快速排序几种实现
- 基本:把等于切分元素的所有元素分到了数组的同一侧,可能会造成递归树倾斜;
- 双指针:把等于切分元素的所有元素等概率地分到了数组的两侧,避免了递归树倾斜,递归树相对平衡;
- 三指针(三路块排):把等于切分元素的所有元素挤到了数组的中间,在有很多元素和切分元素相等的情况下,递归区间大大减少。
pivot 如何选择?
- pivot 选择最左边,先右指针;pivot 选择最右边,先左指针
- pivot 随机?
基本的快速排序
基本的快排实现,pivot 选择 low
基本快速排序可分为以下几步
- 在数组中选择一个基准值(通常为数组的第一个)
- 将数组中小于基准数的数据移动到基准数左边,大于基准数的移到右边(一次快排 partition)
- 对于基准数左右两边的数组,不断重复以上两个过程,直到每个子集只有一个元素,即为全部有序
** 如何将一个数组以基准数为中心分为两部分呢?**
在数组的头部和尾部分别设置一个哨兵,它们同时向对方走去,尾部的哨兵如发现有比基准数小的数就停下,头部的哨兵如发现有比基准数大的数就停下,然后交换两个数;再重新走重复前面的交换流程,直到 2 个哨兵相遇,最后交换基准数和尾哨兵。
案例:
有一数组为 6 1 2 7 9 3 4 5 10 8
,带着这样做为什么可以的态度来看一下演示。
- 6 为基准数,设 i,j 为两哨兵,目前指向首尾两个数(加粗部分)
6 1 2 7 9 3 4 5 10 8
- 两哨兵分别走向对方,直到遇到交换条件,并做交换。
6 1 2 7 9 3 4 5 10 8
6 1 2 5 9 3 4 7 10 8
- 此时来观察交换后的队列,除去基准数,是不是哨兵走过的位置都已部分有序了呢? 左边 1 2 5 都比基准数小,右边 7 10 8 都比基准数大。
1 2 5 9 3 4 7 10 8、
- 继续走直到头尾哨兵相遇,基准数和尾节点交换
6 1 2 5 9 3 4 7 10 8
6 1 2 5 4 3 9 7 10 8
6 1 2 5 4 3 9 7 10 8
3 1 2 5 4 6 9 7 10 8
注意
- 若以第一个元素为基准数,在哨兵互走过程需右边的哨兵先走。
哨兵互走交换的过程就是不断排序的过程。若右边的哨兵先走,不管走多少次,最后相遇时的那个数是小于基准数的。这时与基准数交换,正好分为两个序列。可若是左边的先走,相遇在大于基准数上就不好办了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public static void quickSort(int[] arr) {
// 边界判空
if (arr == null || arr.length <= 1) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int left, int right) {
// 递归结束条件,如果不是>=而是>也是可以的,只是会多处一些无用的判断而已
if (left >= right) {
return;
}
// partition,将一个范围内的数组进行原地排序
int partitionIndex = partition(arr, left, right);
System.out.println("quickSort partitionIndex=" + partitionIndex + ",left=" + left + ",right=" + right);
// 递归:将pivot左半边和右半部分都排序
quickSort(arr, left, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, right);
}
private static int partition(int[] arr, int low, int high) {
// 默认选择low作为pivotIndex
int pivotIndex = low;
int pivot = arr[pivotIndex];
// 将比pivot的小的放左边,比pivot大的放右边
while (low < high) {
// 一定要从high开始,否则会排序不对
// 从high开始,往左找一个比pivot的小的值
// 一定要小于等于,[0, 3, 3, 2, 6, 5, 8, 10, 2],就会出现low=0,high=1,程序走不下去了
while (low < high && arr[high] >= pivot) {
high--;
}
// 从low开始,往右找一个比pivot的大的值
while (low < high && arr[low] <= pivot) {
low++;
}
// 交换low和high
swap(arr, low, high);
}
// 还需要交换pivotIndex和low
swap(arr, pivotIndex, low);
// 最后返回low或high都可以
return low;
}
private static void swap(int[] arr, int m, int n) {
int temp = arr[m];
arr[m] = arr[n];
arr[n] = temp;
}
基本的快排实现,pivot 随机
「快速排序」在遇到特殊测试用例(「顺序数组」或者「逆序数组」)的时候,递归树会退化成链表,时间复杂度会变成 O(N^2)
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public static void quickSort(int[] arr) {
// 边界判空
if (arr == null || arr.length <= 1) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int left, int right) {
// 递归结束条件,如果不是>=而是>也是可以的,只是会多处一些无用的判断而已
if (left >= right) {
return;
}
// partition,将一个范围内的数组进行原地排序
int partitionIndex = partition(arr, left, right);
System.out.println("quickSort partitionIndex=" + partitionIndex + ",left=" + left + ",right=" + right);
// 递归:将pivot左半边和右半部分都排序
quickSort(arr, left, partitionIndex - 1);
quickSort(arr, partitionIndex + 1, right);
}
private static int partition(int[] arr, int low, int high) {
// 在[low,high]随机一个作为基准值
int pivotIndex = low + new Random().nextInt(high - low) + 1;
System.out.println(" -->> partition pivotIndex=" + pivotIndex + ",low=" + low + ",high=" + high);
int pivot = arr[pivotIndex];
// 将基准值和low交换,目前low作为基准值了
swap(arr, pivotIndex, low);
pivotIndex = low;
// 将比pivot的小的放左边,比pivot大的放右边
while (low < high) {
// 一定要从high开始,否则会排序不对
// 从high开始,往左找一个比pivot的小的值
// 一定要小于等于,[0, 3, 3, 2, 6, 5, 8, 10, 2],就会出现low=0,high=1,程序走不下去了
while (low < high && arr[high] >= pivot) {
high--;
}
// 从low开始,往右找一个比pivot的大的值
while (low < high && arr[low] <= pivot) {
low++;
}
// 交换low和high
swap(arr, low, high);
}
// 还需要交换pivotIndex和low
swap(arr, pivotIndex, low);
// 最后返回low或high都可以
return low;
}
private static void swap(int[] arr, int m, int n) {
int temp = arr[m];
arr[m] = arr[n];
arr[n] = temp;
}
快速排序时间复杂度和稳定性
快速排序稳定性
快速排序是不稳定的算法,它不满足稳定算法的定义。
算法稳定性 – 假设在数列中存在
a[i]=a[j]
,若在排序之前,a[i]
在a[j]
前面;并且排序之后,a[i]
仍然在a[j]
前面。则这个排序算法是稳定的!
快排为啥不是稳定的?
比如数组:{1,1,1,2,3,5,4},pivot=1,一趟循环排序后 left=0,right=1,此时会进行交互,但这 2 个位置的元素都为 1,就打破了稳定性了。
快速排序的时间复杂度
快速排序的时间复杂度在最坏情况下是 O(N2),平均的时间复杂度是 O(N*lgN)。
- 最优情况下快速排序的时间复杂度为**O(nlogn) **
每次都恰好五五分,一次递归共需比较 n 次,递归深度为 lgn
为什么最少是 lg(N+1) 次? 快速排序是采用的分治法进行遍历的,我们将它看作一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是 lg(N+1)。因此,快速排序的遍历次数最少是 lg(N+1) 次。
快速排序的空间复杂度
快速排序只是使用数组原本的空间进行排序,所以所占用的空间应该是常量级的,但是由于每次划分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,在一般情况下的空间复杂度为 O(logn),在最差的情况下,若每次只完成了一个元素,那么空间复杂度为 O(n)。所以我们一般认为快速排序的空间复杂度为 O(logn)。