在课程上主要学习算法的思想,在课下通过熟悉“背诵”代码,进行题目的练习达到熟练,练习的方法是将代码全部删除,进行重复写入,循环往复。可以重复三到五次。
快速排序(Quick Sort)基本思想:
通过一趟排序将无序序列分为独立的两个序列,第一个序列的值均比第二个序列的值小。然后递归地排列两个子序列,以达到整个序列有序。
pivot
(这里以当前序列第 1
个元素作为基准数,即 pivot = arr[low]
)。i
,指向当前需要处理的元素位置,需要保证位置 i
之前的元素都小于基准数。初始时,i
指向当前序列的第 2
个元素位置。j
遍历当前序列,如果遇到 arr[j]
小于基准数 pivot
,则将 arr[j]
与当前需要处理的元素 arr[i]
交换,并将 i
向右移动 1
位,保证位置 i
之前的元素都小于基准数。i
之前的元素都小于基准数,第 i - 1
位置上的元素是最后一个小于基准数 pivot
的元素,此位置为基准数最终的正确位置。将基准数与该位置上的元素进行交换。此时,基准数左侧都是小于基准数的元素,右侧都是大于等于基准数的元素。2
步,直到各个子序列只有 1
个元素,则排序结束。[6, 2, 3, 5, 1, 4]
。1
趟排序:
1
个元素 6
作为基准数。2 < 6
,此时 i
与 j
相同,指针 i
向右移动 1
位。3 < 6
,此时 i
与 j
相同,指针 i
向右移动 1
位。5 < 6
,此时 i
与 j
相同,指针 i
向右移动 1
位。1 < 6
,此时 i
与 j
相同,指针 i
向右移动 1
位。4 < 6
,此时 i
与 j
相同,指针 i
向右移动 1
位,i
到达数组末尾。6
与最后 1
位交换位置,则序列变为 [4, 2, 3, 5, 1, 6]
。[4, 2, 3, 5, 1]
和右子序列 []
。2
趟排序:
[4, 2, 3, 5, 1]
中选择当前序列第 1
个元素 4
作为基准数。2 < 4
,此时 i
与 j
相同,指针 i
向右移动 1
位。3 < 4
,此时 i
与 j
相同,指针 i
向右移动 1
位。5 > 4
,不进行操作;1 < 4
,此时 i
指向 5
,j
指向 1
。则将 5
与 1
进行交换,指针 i
向右移动 1
位,i
到达数组末尾。4
与 1
交换位置,则序列变为 [1, 2, 3, 4, 5, 6]
。1
个元素,则排序结束。此时序列变为 [1, 2, 3, 4, 5, 6]
。快速排序算法的时间复杂度主要跟基准数的选择有关。本文中是将当前序列中第 1
个元素作为基准值。
在这种选择下,如果参加排序的元素初始时已经有序的情况下,快速排序方法花费的时间最长。也就是会得到最坏时间复杂度。
在这种情况下,第 1
趟排序经过 n - 1
次比较以后,将第 1
个元素仍然确定在原来的位置上,并得到 1
个长度为 n - 1
的子序列。第 2
趟排序进过 n - 2
次比较以后,将第 2
个元素确定在它原来的位置上,又得到 1
个长度为 n - 2
的子序列。
最终总的比较次数为 ( n − 1 ) + ( n − 2 ) + … + 1 = n ( n − 1 ) 2 (n − 1) + (n − 2) + … + 1 = \frac{n(n − 1)}{2} (n−1)+(n−2)+…+1=2n(n−1)。因此这种情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2),也是最坏时间复杂度。
我们可以改进一下基准数的选择。如果每次我们选中的基准数恰好能将当前序列平分为两份,也就是刚好取到当前序列的中位数。
在这种选择下,每一次都将序列从 n n n 个元素变为 n 2 \frac{n}{2} 2n 个元素。此时的时间复杂度公式为 T ( n ) = 2 × T ( n 2 ) + Θ ( n ) T(n) = 2 \times T(\frac{n}{2}) + \Theta(n) T(n)=2×T(2n)+Θ(n)。根据主定理可以得出 T ( n ) = O ( n × log 2 n ) T(n) = O(n \times \log_2n) T(n)=O(n×log2n),也是最佳时间复杂度。
而在平均情况下,我们可以从当前序列中随机选择一个元素作为基准数。这样,每一次选择的基准数可以看做是等概率随机的。其期望时间复杂度为 O ( n × log 2 n ) O(n \times \log_2n) O(n×log2n),也就是平均时间复杂度。
下面来总结一下:
import random
class Solution:
# 从 arr[low: high + 1] 中随机挑选一个基准数,并进行移动排序
def randomPartition(self, arr: [int], low: int, high: int):
# 随机挑选一个基准数
i = random.randint(low, high)
# 将基准数与最低位互换
arr[i], arr[low] = arr[low], arr[i]
# 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上
return self.partition(arr, low, high)
# 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上
def partition(self, arr: [int], low: int, high: int):
pivot = arr[low] # 以第 1 为为基准数
i = low + 1 # 从基准数后 1 位开始遍历,保证位置 i 之前的元素都小于基准数
for j in range(i, high + 1):
# 发现一个小于基准数的元素
if arr[j] < pivot:
# 将小于基准数的元素 arr[j] 与当前 arr[i] 进行换位,保证位置 i 之前的元素都小于基准数
arr[i], arr[j] = arr[j], arr[i]
# i 之前的元素都小于基准数,所以 i 向右移动一位
i += 1
# 将基准节点放到正确位置上
arr[i - 1], arr[low] = arr[low], arr[i - 1]
# 返回基准数位置
return i - 1
def quickSort(self, arr, low, high):
if low < high:
# 按照基准数的位置,将序列划分为左右两个子序列
pi = self.randomPartition(arr, low, high)
# 对左右两个子序列分别进行递归快速排序
self.quickSort(arr, low, pi - 1)
self.quickSort(arr, pi + 1, high)
return arr
def sortArray(self, nums: List[int]) -> List[int]:
return self.quickSort(nums, 0, len(nums) - 1)
private static int[] quickSort(int[] arr, int left, int right) {
// 递归终止条件,如果左边界大于等于右边界则认为递归结束
if (left >= right) {
return arr;
}
// 设定一个分界值,这里是(left + right)/ 2
int p = arr[left + right >> 1];
// 左右提前预留一个位置
int i = left - 1;
int j = right + 1;
while (i < j) {
// 等效于do while
// 当数值小于分界值时持续遍历,直到找到第一个大于等于分界值的索引
// 如果是逆序则调整两个while循环
while (arr[++i] < p)
;
while (arr[--j] > p)
;
// 交换左右两侧不符合预期的数值
if (i < j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 由于分界值取的是left + right >> 1,因此递归取的是left,j j + 1,right
quickSort(arr, left, j);
quickSort(arr, j + 1, right);
return arr;
}
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j)
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
归并排序(Merge Sort)基本思想:
采用经典的分治策略,先递归地将当前序列平均分成两半。然后将有序序列两两合并,最终合并成一个有序序列。
1
。
mid
,从中心位置将序列分成左右两个子序列 left_arr
、right_arr
。left_arr
、right_arr
分别进行递归分割。n
个长度均为 1
的有序子序列。1
的有序子序列开始,依次进行两两归并,直到合并成一个长度为 n
的有序序列。
arr
存放归并后的有序数组。left_i
、right_i
分别指向两个有序子序列 left_arr
、right_arr
的开始位置。arr
中,并将指针移动到下一位置。3
,直到某一指针到达子序列末尾。arr
中。arr
。[6, 2, 1, 3, 7, 5, 4, 8]
。[6, 2, 1, 3]
,[7, 5, 4, 8]
。[6, 2]
,[1, 3]
,[7, 5]
,[4, 8]
。[6]
,[2]
,[1]
,[3]
,[7]
,[5]
,[4]
,[8]
。8
个长度为 1
的子序列,即 [6]
,[2]
,[1]
,[3]
,[7]
,[5]
,[4]
,[8]
。1
趟排序:将子序列中的有序子序列两两归并,归并后的子序列为:[2, 6]
,[1, 3]
,[5, 7]
,[4, 8]
。2
趟排序:将子序列中的有序子序列两两归并,归并后的子序列为:[1, 2, 3, 6]
,[4, 5, 7, 8]
。3
趟排序:将子序列中的有序子序列两两归并,归并后的子序列为:[1, 2, 3, 4, 5, 6, 7, 8]
。得到长度为 n
的有序序列,排序结束。归并排序方法需要用到与参加排序的序列同样大小的辅助空间。因此算法的空间复杂度为 O ( n ) O(n) O(n)。
归并排序算法的时间复杂度等于归并趟数与每一趟归并的时间复杂度乘积。子算法 merge(left_arr, right_arr):
的时间复杂度是
O ( n ) O(n) O(n)
,因此,归并排序算法总的时间复杂度为 O ( n × log 2 n ) O(n \times \log_2 n) O(n×log2n)。
空间复杂度:
O ( n ) O(n) O(n)
。归并排序方法需要用到与参加排序的序列同样大小的辅助空间。因此算法的空间复杂度为 O ( n ) O(n) O(n)。
排序稳定性:归并排序算法是一种 稳定排序算法。
merge(left_arr, right_arr):
算法能够使前一个序列中那个相同元素先被复制,从而确保这两个元素的相对次序不发生改变。class Solution:
def merge(self, left_arr, right_arr): # 归并过程
arr = []
left_i, right_i = 0, 0
while left_i < len(left_arr) and right_i < len(right_arr):
# 将两个有序子序列中较小元素依次插入到结果数组中
if left_arr[left_i] < right_arr[right_i]:
arr.append(left_arr[left_i])
left_i += 1
else:
arr.append(right_arr[right_i])
right_i += 1
while left_i < len(left_arr):
# 如果左子序列有剩余元素,则将其插入到结果数组中
arr.append(left_arr[left_i])
left_i += 1
while right_i < len(right_arr):
# 如果右子序列有剩余元素,则将其插入到结果数组中
arr.append(right_arr[right_i])
right_i += 1
return arr # 返回排好序的结果数组
def mergeSort(self, arr): # 分割过程
if len(arr) <= 1: # 数组元素个数小于等于 1 时,直接返回原数组
return arr
mid = len(arr) // 2 # 将数组从中间位置分为左右两个数组。
left_arr = self.mergeSort(arr[0: mid]) # 递归将左子序列进行分割和排序
right_arr = self.mergeSort(arr[mid:]) # 递归将右子序列进行分割和排序
return self.merge(left_arr, right_arr) # 把当前序列组中有序子序列逐层向上,进行两两合并。
def sortArray(self, nums: List[int]) -> List[int]:
return self.mergeSort(nums)
private static int[] mergeSort(int[] arr, int left, int right) {
// 递归终止条件,如果左边界大于等于右边界则认为递归结束
if (left >= right) {
return arr;
}
// 设定一个分界值,这里是(left + right)/ 2
int mid = left + right >> 1;
// 切割
arr = mergeSort(arr, left, mid);
arr = mergeSort(arr, mid + 1, right);/
// 归并,长度刚好是 left 到 right
int[] temp = new int[right - left + 1];
int i = left;
int j = mid + 1;
// 用来归并的索引
int k = 0;
while (i <= mid && j <= right) {
// 如果是逆序则调整if条件
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= right) {
temp[k++] = arr[j++];
}
// 根据归并后的数组重新赋值排序后的数组
for (i = left, j = 0; i <= right; i++, j++) {
arr[i] = temp[j];
}
return arr;
}
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j)
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}