一、快速排序
荷兰国旗问题
问题一
给定一个数组arr
,和一个数num
,请把小于等于num
的数放在数组的左边,大于num
的数放在数组的右边。要求额外空间复杂度
O
(
1
)
O(1)
O(1),时间复杂度
O
(
N
)
O(N)
O(N)
【解题思路】:准备一个指针i
和一个指针P
,P
用于划分小于等于区域,从数组的首部开始,将当前数arr[i]
与num
作比较:
arr[i] <= num
,则arr[i]
和小于等于区域的下一个数arr[P+1]
交换,小于等于区域右扩,即P++
,同时i++
arr[i] > num
,则i++
问题二 (荷兰国旗问题)
给定一个数组arr
,和一个数num
,请把小于num
的数放在数组的左边,等于num
的数放在数组的中间,大于num
的数放在数组的右边。要求额外空间复杂度
O
(
1
)
O(1)
O(1),时间复杂度
O
(
N
)
O(N)
O(N)
【解题思路】:准备三个指针i,p1,p2
,p1
用于划分小于区域,p2
用于划分大于区域,[p1, p2]
为等于区域,从数组的首部开始,将当前数arr[i]
与num
作比较:
arr[i] < num
,则arr[i]
和小于区域的下一个数交换,小于区域向右扩,i++
arr[i] == num
,则i++
arr[i] > num
,则arr[i]
和大于区域前一个数交换,大于区域左扩,i
不变,因为交换过来的数还没有作判断。
快速排序和这两个问题的思想类似,核心思想都是大于/小于区域的划分
快速排序算法思想
- 从数列中随机挑出一个元素,称为 “基准”(pivot)
- 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以放中间)。在这个分区结束之后,与该基准相同的数就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
Java实现
public class QuickSort {
/**
* 快速排序算法思想
* 1. 从数列中随机挑出一个元素,称为"基准"(pivot)
* 2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以放中间)。在这个分区结束之后,与该基准相同的数就处于数列的中间位置。这个称为分区(partition)操作。
* 3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
*/
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
// arr[l~r]排好序
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
// 首先等概率随机选一个数,将其与最右位置的数交换。
swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
int[] p = partition(arr, l, r);
quickSort(arr, l, p[0] - 1);
quickSort(arr, p[0] + 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, more = r - 1;
while (l <= more) {
if (arr[l] < arr[r]) {
swap(arr, l++, less++);
} else if (arr[l] > arr[r]) {
swap(arr, l, more--);
} else {
l++;
}
}
swap(arr, r, --more);
return new int[]{less, more};
}
public static void swap(int[] arr, int src, int dest) {
int temp = arr[src];
arr[src] = arr[dest];
arr[dest] = temp;
}
}
复杂度分析:由于基准值是随机选择的,所以有可能选的基准值就是真正的中值,此时算法复杂度最低;但也有可能选到边界值,此时算法复杂度最差(接近冒泡排序);所以综合考虑全部可能性,并利用数学方法求期望可得平均复杂度为 O ( N ∗ l o g N ) O(N*log\ N) O(N∗log N)
实战:颜色分类
- leetcode 原题:75. 颜色分类 - 力扣(LeetCode)
- 难度等级: Medium
二、堆排序
堆结构
堆是一种特殊的完全二叉树(complete binary tree)。完全二叉树的一个 “优秀” 的性质是,除了最底层之外,每一层都是满的,最底层所有的数都靠左,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。
对于给定的某个结点的下标 i
,可以很容易的计算出这个结点的父结点、孩子结点的下标:
Parent(i) = floor(i - 1 / 2)
,i
的父节点下标Left(i) = 2i + 1
,i
的左子节点下标Right(i) = 2i + 2
,i
的右子节点下标
二叉堆一般分为两种:最大堆和最小堆
- 最大堆:
最大堆中的最大元素值出现在根结点(堆顶)
堆中每个父节点的元素值都大于等于其孩子结点(如果存在) - 最小堆:
最小堆中的最小元素值出现在根结点(堆顶)
堆中每个父节点的元素值都小于等于其孩子结点(如果存在)
堆的核心操作有两个:heapify(能否向下移动),heapInsert(能否向上移动)
堆的建立和维护
考虑两个问题:
-
删除堆顶元素后,如何调整数组成为新堆?
把最后一个元素(代号A)移动到根元素的位置,此时A一定小于其某个子节点,因此需要执行heapify向下移动
-
插入堆顶元素后,如何调整数组成为新堆?
把新元素放在末尾,然后和其父节点做比较,执行heapInsert向上移动。
堆如何建立?
可以将第一个元素看作一个堆,然后不断向其中添加新元素,并不断进行heapInsert。
堆排序算法描述
对数组建立最大堆,然后将堆最后一个位置的值与根节点的值交换,此时在最后一个位置输出最大值,即调整堆的大小使其减一,并对根节点执行heapify操作,对调整后的堆重复上述操作。
由于每次输出的最大元素会腾出第一个空间,因此,恰好可以放置这样的元素而不需要额外空间。
public class HeapSort {
/**
* 堆排序算法描述
* 对数组建立最大堆,然后将堆最后一个位置的值与根节点的值交换,此时在最后一个位置输出最大值,即调整堆的大小使其减一,并对根节点执行heapify操作,对调整后的堆重复上述操作。
*/
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 建立大根堆 O(N)
for (int i = 1; i < arr.length; i++) {
// O(log N)
heapInsert(arr, i);
}
// // 建立大根堆更快一点的方法 O(N)
// for (int i = arr.length - 1; i >= 0; i--) {
// heapify(arr, i, arr.length);
// }
// 输出排序后的数组
int heapSize = arr.length;
// O(N)
while (heapSize > 0) {
// O(1)
swap(arr, 0, --heapSize);
// O(logN)
heapify(arr, 0, heapSize);
}
}
/**
* 某个数现在处于index位置,能否继续向上移动
*
* @param arr 堆
* @param index 该数在arr中的下标
*/
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位置,能否向下移动
*
* @param arr 堆
* @param index 该数在arr中的下标
* @param heapSize 堆的大小,用于判断是否有左/右孩子
*/
public static void heapify(int[] arr, int index, int heapSize) {
// 左孩子的下标
int left = 2 * index + 1;
// 是否还有孩子
while (left < heapSize) {
// 左孩子与右孩子比较
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
// 大孩子与父亲比较
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
return;
}
swap(arr, largest, index);
index = largest;
left = 2 * index + 1;
}
}
public static void swap(int[] arr, int src, int dest) {
int temp = arr[src];
arr[src] = arr[dest];
arr[dest] = temp;
}
}
为什么上述代码中第二种建立大根堆的过程,时间复杂度可以为 O ( N ) O(N) O(N)呢?
- 由二叉树叶节点开始逐步对每个节点执行heapify过程也是可以将数组大根堆化,但其实对于叶节点heapify不进行任何操作,因为无法再向下移动了,所以heapify执行的次数实际只有 N 2 \frac{N}{2} 2N次(即从非叶节点处),将这 N 2 \frac{N}{2} 2N个节点执行heapify的复杂度累加起来就是 O ( N ) O(N) O(N)
但推排序总体的复杂度不变,仍是 O ( N l o g N ) O(NlogN) O(NlogN)
实战:数组中的第K个最大元素
- leetcode 原题:215. 数组中的第K个最大元素 - 力扣(LeetCode)
- 难度等级: Medium