快排和归并排序算法学习

1.1 基础算法(一)

​ 在课程上主要学习算法的思想,在课下通过熟悉“背诵”代码,进行题目的练习达到熟练,练习的方法是将代码全部删除,进行重复写入,循环往复。可以重复三到五次。

1. 快速排序算法思想

快速排序(Quick Sort)基本思想

通过一趟排序将无序序列分为独立的两个序列,第一个序列的值均比第二个序列的值小。然后递归地排列两个子序列,以达到整个序列有序。

2. 快速排序算法步骤

  1. 从序列中找到一个基准数 pivot(这里以当前序列第 1 个元素作为基准数,即 pivot = arr[low])。
  2. 使用双指针,将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧:
    1. 使用指针 i,指向当前需要处理的元素位置,需要保证位置 i 之前的元素都小于基准数。初始时,i 指向当前序列的第 2 个元素位置。
    2. 使用指针 j 遍历当前序列,如果遇到 arr[j] 小于基准数 pivot,则将 arr[j] 与当前需要处理的元素 arr[i] 交换,并将 i 向右移动 1 位,保证位置 i 之前的元素都小于基准数。
    3. 最后遍历完,此时位置 i 之前的元素都小于基准数,第 i - 1 位置上的元素是最后一个小于基准数 pivot 的元素,此位置为基准数最终的正确位置。将基准数与该位置上的元素进行交换。此时,基准数左侧都是小于基准数的元素,右侧都是大于等于基准数的元素。
    4. 然后将序列拆分为左右两个子序列。
  3. 对左右两个子序列分别重复第 2 步,直到各个子序列只有 1 个元素,则排序结束。

3. 快速排序动画演示

快排和归并排序算法学习_第1张图片

  1. 初始序列为:[6, 2, 3, 5, 1, 4]
  2. 1 趟排序:
    1. 选择当前序列第 1 个元素 6 作为基准数。
    2. 从左到右遍历序列:
      1. 遇到 2 < 6,此时 ij 相同,指针 i 向右移动 1 位。
      2. 遇到 3 < 6,此时 ij 相同,指针 i 向右移动 1 位。
      3. 遇到 5 < 6,此时 ij 相同,指针 i 向右移动 1 位。
      4. 遇到 1 < 6,此时 ij 相同,指针 i 向右移动 1 位。
      5. 遇到 4 < 6,此时 ij 相同,指针 i 向右移动 1 位,i 到达数组末尾。
    3. 最终将基准值 6 与最后 1 位交换位置,则序列变为 [4, 2, 3, 5, 1, 6]
    4. 将序列分为左子序列 [4, 2, 3, 5, 1] 和右子序列 []
  3. 2 趟排序:
    1. 左子序列 [4, 2, 3, 5, 1] 中选择当前序列第 1 个元素 4 作为基准数。
    2. 从到右遍历左子序列:
      1. 遇到 2 < 4,此时 ij 相同,指针 i 向右移动 1 位。
      2. 遇到 3 < 4,此时 ij 相同,指针 i 向右移动 1 位。
      3. 遇到 5 > 4,不进行操作;
      4. 遇到 1 < 4,此时 i 指向 5j 指向 1。则将 51 进行交换,指针 i 向右移动 1 位,i 到达数组末尾。
    3. 最终将基准值 41 交换位置,则序列变为 [1, 2, 3, 4, 5, 6]
  4. 依次类推,重复选定基准数,并将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。直到各个子序列只有 1 个元素,则排序结束。此时序列变为 [1, 2, 3, 4, 5, 6]

4. 快速排序算法分析

快速排序算法的时间复杂度主要跟基准数的选择有关。本文中是将当前序列中第 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} (n1)+(n2)++1=2n(n1)。因此这种情况下的时间复杂度为 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),也就是平均时间复杂度。

下面来总结一下:

  • 最佳时间复杂度 O ( n × log ⁡ 2 n ) O(n \times \log_2n) O(n×log2n)。每一次选择的基准数都是当前序列的中位数,此时算法时间复杂度满足的递推式为 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 2 ) O(n^2) O(n2)。每一次选择的基准数都是序列的最终位置上的值,此时算法时间复杂度满足的递推式为 T ( n ) = T ( n − 1 ) + Θ ( n ) T(n) = T(n - 1) + \Theta(n) T(n)=T(n1)+Θ(n),累加可得 T ( n ) = O ( n 2 ) T(n) = O(n^2) T(n)=O(n2)
  • 平均时间复杂度 O ( n × log ⁡ 2 n ) O(n \times \log_2n) O(n×log2n)。在平均情况下,每一次选择的基准数可以看做是等概率随机的。其期望时间复杂度为 O ( n × log ⁡ 2 n ) O(n \times \log_2n) O(n×log2n)
  • 空间复杂度 O ( n ) O(n) O(n)。无论快速排序算法递归与否,排序过程中都需要用到堆栈或其他结构的辅助空间来存放当前待排序序列的首、尾位置。最坏的情况下,空间复杂度为 O ( n ) O(n) O(n)。如果对算法进行一些改写,在一趟排序之后比较被划分所得到的两个子序列的长度,并且首先对长度较短的子序列进行快速排序,这时候需要的空间复杂度可以达到 O ( l o g 2 n ) O(log_2 n) O(log2n)
  • 排序稳定性:快速排序是一种 不稳定排序算法

5. 快速排序代码实现

1.Python代码实现

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)

2.JAVA代码实现

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;
}

3.c++代码实现

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);
}

1. 归并排序算法思想

归并排序(Merge Sort)基本思想

采用经典的分治策略,先递归地将当前序列平均分成两半。然后将有序序列两两合并,最终合并成一个有序序列。

2. 归并排序算法步骤

  1. 分割过程:先递归地将当前序列平均分成两半,直到子序列长度为 1
    1. 找到序列中心位置 mid,从中心位置将序列分成左右两个子序列 left_arrright_arr
    2. 对左右两个子序列 left_arrright_arr 分别进行递归分割。
    3. 最终将数组分割为 n 个长度均为 1 的有序子序列。
  2. 归并过程:从长度为 1 的有序子序列开始,依次进行两两归并,直到合并成一个长度为 n 的有序序列。
    1. 使用数组变量 arr 存放归并后的有序数组。
    2. 使用两个指针 left_iright_i 分别指向两个有序子序列 left_arrright_arr 的开始位置。
    3. 比较两个指针指向的元素,将两个有序子序列中较小元素依次存入到结果数组 arr 中,并将指针移动到下一位置。
    4. 重复步骤 3,直到某一指针到达子序列末尾。
    5. 将另一个子序列中的剩余元素存入到结果数组 arr 中。
    6. 返回归并后的有序数组 arr

3. 归并排序动画演示

  1. 初始序列为 [6, 2, 1, 3, 7, 5, 4, 8]
  2. 将序列分解为 [6, 2, 1, 3][7, 5, 4, 8]
  3. 将序列分解为 [6, 2][1, 3][7, 5][4, 8]
  4. 将序列分为为 [6][2][1][3][7][5][4][8]
  5. 将序列看做是 8 个长度为 1 的子序列,即 [6][2][1][3][7][5][4][8]
  6. 1 趟排序:将子序列中的有序子序列两两归并,归并后的子序列为:[2, 6][1, 3][5, 7][4, 8]
  7. 2 趟排序:将子序列中的有序子序列两两归并,归并后的子序列为:[1, 2, 3, 6][4, 5, 7, 8]
  8. 3 趟排序:将子序列中的有序子序列两两归并,归并后的子序列为:[1, 2, 3, 4, 5, 6, 7, 8]。得到长度为 n 的有序序列,排序结束。

4. 归并排序算法分析

归并排序方法需要用到与参加排序的序列同样大小的辅助空间。因此算法的空间复杂度为 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): 算法能够使前一个序列中那个相同元素先被复制,从而确保这两个元素的相对次序不发生改变。

5. 归并排序代码实现

1.python 代码实现

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)

2.JAVA代码实现

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;
}

3.c++代码实现

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);
}


你可能感兴趣的:(算法学习,排序算法,学习,python)