Featured image of post Sort

Sort

排序

 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
#include <iostream>
#include <algorithm>
#include <vector>

void quick_sort(std::vector<int>& nums, int l, int r);
void merge_sort(std::vector<int>& nums, int l, int r);
void select_sort(std::vector<int>& nums);
void insert_sort(std::vector<int>& nums);
void binary_sort(std::vector<int>& nums);
void bubble_sort(std::vector<int>& nums);

int main()
{
  int n;
  scanf("%d", &n);

  std::vector<int> nums(n);
  for (int i = 0; i < n; ++ i)
    scanf("%d", &nums[i]);

  quick_sort(nums, 0, n - 1);
  merge_sort(nums, 0, n - 1);
  select_sort(nums);
  insert_sort(nums);
  binary_sort(nums);
  bubble_sort(nums);

  for (int i = 0; i < n; ++ i)
    printf("%d ", nums[i]);

  return 0;
}

快速排序

  1. 选取基准值(pivot):x = nums[(l + r) >> 1]
  2. 双指针划分数组:
    • i 从左往右,找到第一个 ≥ pivot 的数
    • j 从右往左,找到第一个 ≤ pivot 的数
    • 交换 nums[i]nums[j],直到 i >= j
  3. 递归处理左右子数组:
    • 递归排序 [l, j](左半部分)
    • 递归排序 [j+1, r](右半部分)

复杂度

  1. 时间复杂度
    • 最佳情况(每次都均匀划分):O(nlogn)
    • 平均情况(随机数据):O(nlogn)
    • 最坏情况(已排序数组,划分极端不均匀):O(n²)
  2. 空间复杂度
    • 最坏情况(单侧划分,递归深度 O(n))):O(n)
    • 平均情况(均匀划分,递归深度 O(logn))):O(logn)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void quick_sort(std::vector<int>& nums, int l, int r)
{
  if (l >= r) return;  // 递归终止条件

  int i = l - 1, j = r + 1, x = nums[(l + r) >> 1]; // 取中间值作为基准
  while (i < j)
  {
    do ++i; while (nums[i] < x); // 找到左侧大于等于 pivot 的元素
    do --j; while (nums[j] > x); // 找到右侧小于等于 pivot 的元素
    if (i < j)
      std::swap(nums[i], nums[j]); // 交换不符合条件的元素
  }

  quick_sort(nums, l, j);   // 递归排序左半部分
  quick_sort(nums, j + 1, r); // 递归排序右半部分
}

归并排序

  1. 递归地将数组划分为左右两部分:
    • 直到子数组长度为 1,即 l >= r,终止递归
  2. 分别对左右两部分进行排序:
    • 递归调用 merge_sort(nums, l, mid)
    • 递归调用 merge_sort(nums, mid + 1, r)
  3. 合并两个有序数组:
    • 使用双指针方法合并 nums[l ~ mid]nums[mid+1 ~ r]
    • 额外使用一个 tmp 数组存储排序后的结果,再复制回 nums

复杂度

  1. 时间复杂度
    • 每次划分数组需要 O(logn) 次递归
    • 每次合并两个有序数组需要 O(n) 时间
    • 总时间复杂度:O(nlogn)
  2. 空间复杂度
    • 需要额外 O(n) 的辅助数组 tmp 存储合并结果
    • 总空间复杂度:O(n)
 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
void merge_sort(std::vector<int>& nums, int l, int r)
{
  if (l >= r) return;  // 递归终止条件:子数组长度为1

  int mid = (l + r) >> 1;  // 取中间点
  merge_sort(nums, l, mid);  // 递归排序左半部分
  merge_sort(nums, mid + 1, r);  // 递归排序右半部分

  std::vector<int> tmp(nums.size());  // 临时数组用于合并
  int i = l, j = mid + 1, k = 0;  // i 遍历左半部分,j 遍历右半部分

  // 归并两个有序子数组
  while (i <= mid && j <= r)
  {
    if (nums[i] <= nums[j])
      tmp[k++] = nums[i++];  // 选较小的数加入临时数组
    else
      tmp[k++] = nums[j++];
  }

  // 处理剩余元素
  while (i <= mid) tmp[k++] = nums[i++];
  while (j <= r) tmp[k++] = nums[j++];

  // 复制回原数组
  for (int i = l, k = 0; i <= r; ++i, ++k)
    nums[i] = tmp[k];
}

堆排序

  1. 建堆:从最后一个非叶子节点 n/2 开始自上而下堆化 down()
    • 建立大根堆:
      • 满足大根堆的性质:父节点大于等于子节点
    • 建立小根堆:
      • 满足小根堆的性质:父节点小于等于子节点
  2. 排序:
    • 交换堆顶元素(最大值/最小值)堆尾元素,然后调整剩余元素继续保持大根堆/小根堆
    • 每次取出堆顶元素,最终形成升序/降序排列

复杂度

  1. 时间复杂度
    • 建堆:O(n)
    • 排序调整堆:每次 down() 需要 O(log n),执行 n 次,总计 O(nlogn)
    • 总时间复杂度:O(nlogn)
  2. 空间复杂度
    • 堆排序在原数组上进行排序,没有额外的数组存储,空间复杂度 O(1)
 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
#include <iostream>
#include <algorithm>
#include <vector>

// 维护小根堆性质 (较大值向下调整)
void big_down(std::vector<int>& nums, int u, int sz)
{
    int t = u;
    if (u * 2 <= sz && nums[t] > nums[u * 2])  // 左子节点较小
        t = u * 2;
    if (u * 2 + 1 <= sz && nums[t] > nums[u * 2 + 1])  // 右子节点较小
        t = u * 2 + 1;

    if (t != u)  // 需要交换
    {
        std::swap(nums[t], nums[u]);
        big_down(nums, t, sz);  // 递归调整
    }
}

// 维护大根堆性质 (较小值向下调整)
void small_down(std::vector<int>& nums, int u, int sz)
{
    int t = u;
    if (u * 2 <= sz && nums[t] < nums[u * 2])  // 左子节点较大
        t = u * 2;
    if (u * 2 + 1 <= sz && nums[t] < nums[u * 2 + 1])  // 右子节点较大
        t = u * 2 + 1;

    if (t != u)  // 需要交换
    {
        std::swap(nums[t], nums[u]);
        small_down(nums, t, sz);  // 递归调整
    }
}

int main()
{
    int n;
    scanf("%d", &n);

    // 下标从1开始存储堆,避免叶子结点边界处理
    std::vector<int> nums(n + 1);
    for (int i = 1; i <= n; ++i)
    {
        scanf("%d", &nums[i]);
    }

    // 建堆 (从最后一个非叶子节点开始)
    for (int i = n / 2; i >= 1; --i)
    {
        big_down(nums, i, n);
    }

    int sz = n;
    // 堆排序
    for (int i = 0; i < n; ++i)
    {
        printf("%d ", nums[1]);  // 输出堆顶元素
        std::swap(nums[1], nums[sz]);  // 交换堆顶和堆尾元素
        -- sz;  // 缩小堆的范围
        big_down(nums, 1, sz);  // 重新调整堆
    }
    printf("\n");

    return 0;
}

选择排序

  1. 在第 i 轮中,从 in - 1 中选择最小值,并将其与 i 位置交换
  2. 进行 n - 1 轮排序后,数组有序

复杂度

  1. 时间复杂度 O(n^2)
  2. 空间复杂度 O(1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void select_sort(std::vector<int>& nums)
{
  for (int i = 0; i < nums.size() - 1; ++i)
  {
    int min_idx = i;  // 记录当前最小值的索引
    for (int j = i + 1; j < nums.size(); ++j)
      if (nums[min_idx] > nums[j])  // 找到更小的元素
        min_idx = j;

    std::swap(nums[i], nums[min_idx]);  // 交换当前元素与最小值
  }
}

插入排序

  1. 从第二个元素开始,假设第一个元素已经是有序的
  2. 比较当前元素与已排序部分的元素,找到合适的插入位置
  3. 将当前元素插入到合适的位置,并调整数组中元素的位置

复杂度

  1. 时间复杂度
    • 最佳情况(数组已经有序):O(n)
      • 只需进行一次比较即可,每个元素都只需插入一次
    • 最坏情况(数组逆序):O(n^2)
      • 每次插入时需要将前面的所有元素都移动一遍
    • 平均时间复杂度:O(n^2)
  2. 空间复杂度
    • 空间复杂度:O(1),插入排序是原地排序,无需额外空间
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void insert_sort(std::vector<int>& nums)
{
  for (int i = 1; i < nums.size(); ++ i)  // 从第二个元素开始
  {
    int x = nums[i], j = i;  // 保存当前元素并从当前位置开始向前检查
    while (j >= 1 && x < nums[j - 1])  // 查找插入位置
    {
      nums[j] = nums[j - 1];  // 向右移动元素
      -- j;  // 移动索引
    }
    nums[j] = x;  // 插入当前元素
  }
}

折半插入排序

  1. 从第二个元素开始,假设第一个元素已经是有序的
  2. 使用二分查找在已排序部分找到插入的位置
  3. 将当前位置之后的元素向后移动一位,并将当前元素插入到找到的位置

复杂度

  1. 时间复杂度
    • 最佳情况(数组已经有序):O(n)
      • 每次只需进行一次比较即可,每个元素都只需插入一次
    • 最坏情况(数组逆序):O(n^2)
      • 二分查找将时间复杂度降低为 O(logn),但每次插入仍然需要将元素移动,整体仍为 O(n^2)
    • 平均时间复杂度:O(n^2)
  2. 空间复杂度
    • 空间复杂度:O(1),原地排序,不需要额外的空间
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void binary_sort(std::vector<int>& nums)
{
  for (int i = 1; i < nums.size(); ++ i)  // 从第二个元素开始
  {
    if (nums[i - 1] < nums[i])  // 如果当前元素比前一个元素大,则跳过
      continue;

    int x = nums[i];  // 保存当前元素
    int l = 0, r = i - 1;  // 设置二分查找的左右边界

    while (l < r)  // 二分查找插入位置
    {
      int mid = (l + r) >> 1;
      if (x < nums[mid])
        r = mid;  // 当前元素小于mid,右边界左移
      else
        l = mid + 1;  // 当前元素大于mid,左边界右移
    }

    for (int j = i - 1; j >= r; -- j)  // 移动元素
      nums[j + 1] = nums[j];
    nums[r] = x;  // 插入当前元素到正确的位置
  }
}

冒泡排序

冒泡排序主要通过重复交换相邻的元素来将较小的元素冒泡到数组的前端,较大的元素则被“推”到数组的末端

  1. 比较相邻元素:从数组的第一个元素开始,逐一比较相邻的两个元素。如果它们的顺序不正确(即前一个元素大于后一个元素),则交换它们的位置
  2. 冒泡操作:经过一轮遍历后,最小的元素被冒泡到数组的前端
  3. 重复遍历:继续对剩余部分进行相同的操作,直到整个数组排序完成
  4. 提前终止:如果某一轮遍历中没有发生交换,说明数组已经是有序的,可以提前终止排序

复杂度

  1. 时间复杂度分析
    • 最坏时间复杂度:O(n^2),即当数组完全逆序时,每一轮都需要交换,每一对相邻元素都会进行比较和交换
    • 最佳时间复杂度:O(n),即当数组已经是有序时,只需进行一次遍历,不需要交换
    • 平均时间复杂度:O(n^2),适用于随机排列的数组
  2. 空间复杂度
    • 空间复杂度:O(1),原地排序,不需要额外的存储空间
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void bubble_sort(std::vector<int>& nums)
{
  for (int i = 0; i < nums.size() - 1; ++ i) // 外层循环控制排序的轮数
  {
    bool is_swap = false; // 用于检查是否发生交换
    for (int j = nums.size() - 1; j > i; -- j) // 内层循环进行元素的比较与交换
    {
      if (nums[j] < nums[j - 1]) // 如果前一个元素大于后一个元素
      {
        std::swap(nums[j], nums[j - 1]); // 交换元素
        is_swap = true; // 标记发生了交换
      }
    }

    if (!is_swap) // 如果没有发生交换,说明数组已经有序,可以提前结束
      break;
  }
}
Built with Hugo
Theme Stack designed by Jimmy