当前位置:   article > 正文

【LeetCode】排序精选12题

【LeetCode】排序精选12题

目录

排序:

1. 合并区间(中等)

2. 数组的相对排序(简单)

快速排序:

1. 颜色分类(中等)

2. 排序数组(中等)

3. 数组中的第K个最大元素(中等)

4. 最小K个数(中等)

归并排序:

1. 排序数组(中等)

2. 交易逆序对的总数(困难)

3. 计算右侧小于当前元素的个数(困难)

4. 翻转对(困难)

5. 排序链表(中等)

6. 合并 K 个升序链表(困难)

6.1 递归解法(归并)

6.2 迭代解法(堆)


排序:

1. 合并区间(中等)

先将所有区间按照起始位置排序。如果当前区间的起始位置 <= 前一个区间的结束位置,则可以合并,前一个区间不变,当前区间变为合并后的区间。如果当前区间和前一个区间不能合并,则把前一个区间加到答案中。遍历完成后,还要把最后一个区间加到答案中。

  1. class Solution {
  2. public:
  3. vector<vector<int>> merge(vector<vector<int>>& intervals) {
  4. int n = intervals.size();
  5. sort(intervals.begin(), intervals.end());
  6. vector<vector<int>> ans;
  7. for (int i = 1; i < n; i++)
  8. {
  9. if (intervals[i][0] <= intervals[i - 1][1])
  10. {
  11. // 合并
  12. intervals[i][0] = min(intervals[i][0], intervals[i - 1][0]);
  13. intervals[i][1] = max(intervals[i][1], intervals[i - 1][1]);
  14. }
  15. else
  16. {
  17. ans.push_back(intervals[i - 1]);
  18. }
  19. }
  20. ans.push_back(intervals[n - 1]);
  21. return ans;
  22. }
  23. };

2. 数组的相对排序(简单)

对于arr1的两个元素:

  • 如果都在arr2中,按下标进行比较,下标小的靠前
  • 如果一个在arr2中,一个不在arr2中,在arr2中的靠前
  • 如果都不在arr2中,直接比较两个元素的大小即可

使用哈希表记录arr2中的元素,key——arr2中的某元素,value——该元素对应的下标。

使用sort函数搭配lambda表达式实现自定义排序。

  1. class Solution {
  2. public:
  3. vector<int> relativeSortArray(vector<int>& arr1, vector<int>& arr2) {
  4. unordered_map<int, int> hash;
  5. // 初始化哈希表
  6. for (int i = 0; i < arr2.size(); i++)
  7. {
  8. hash[arr2[i]] = i;
  9. }
  10. // 自定义排序
  11. sort(arr1.begin(), arr1.end(), [&](int e1, int e2)
  12. {
  13. if (hash.count(e1) && hash.count(e2))
  14. return hash[e1] < hash[e2];
  15. else if (hash.count(e1))
  16. return true;
  17. else if (hash.count(e2))
  18. return false;
  19. else
  20. return e1 < e2;
  21. });
  22. return arr1;
  23. }
  24. };

快速排序:

1. 颜色分类(中等)

类比移动零

得出数组分三块的示意图:

  • left指向0序列的最后一个,因此初始化为-1
  • i用来扫描数组,因此初始化为0
  • right指向2序列的第一个,因此初始化为n
  1. class Solution {
  2. public:
  3. void sortColors(vector<int>& nums) {
  4. int n = nums.size();
  5. int left = -1;
  6. int right = n;
  7. int i = 0;
  8. while (i < right)
  9. {
  10. if (nums[i] == 0)
  11. {
  12. swap(nums[++left], nums[i++]);
  13. }
  14. else if (nums[i] == 1)
  15. {
  16. i++;
  17. }
  18. else
  19. {
  20. swap(nums[--right], nums[i]);
  21. }
  22. }
  23. }
  24. };

2. 排序数组(中等)

普通快排会超时,必须优化,可以使用数组划分三块的思想搭配随机选择基准元素的方法。

在待排序表中随机取一个元素key作为基准值,通过一趟排序将待排序表划分为独立的三部分区间[0, left],区间[left + 1, right - 1],区间[right, n - 1],使得区间[0, left]中的所有元素小于key,区间[left + 1, right - 1]中的所有元素等于key,区间[right, n - 1]中的所有元素大于key,则等于key的元素都放在了其最终位置——区间[left + 1, right - 1]上,这个过程称为一次划分。然后分别递归地对两个没排好序的子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。

  1. class Solution {
  2. public:
  3. vector<int> sortArray(vector<int>& nums) {
  4. srand((unsigned int)time(nullptr)); // 设置随机数种子
  5. quickSort(nums, 0, nums.size() - 1);
  6. return nums;
  7. }
  8. private:
  9. void quickSort(vector<int>& nums, int begin, int end)
  10. {
  11. // 递归出口
  12. if (begin >= end)
  13. return;
  14. // 划分左中右三个子区间
  15. vector<int> ret = partition(nums, begin, end);
  16. int left = ret[0];
  17. int right = ret[1];
  18. // 递归排序左右两个子区间
  19. quickSort(nums, begin, left);
  20. quickSort(nums, right, end);
  21. }
  22. vector<int> partition(vector<int>& nums, int begin, int end)
  23. {
  24. // 随机取key
  25. int key = nums[rand() % (end - begin + 1) + begin];
  26. // 数组分三块
  27. int left = begin - 1;
  28. int right = end + 1;
  29. int i = begin;
  30. while (i < right)
  31. {
  32. if (nums[i] < key)
  33. {
  34. swap(nums[++left], nums[i++]);
  35. }
  36. else if (nums[i] == key)
  37. {
  38. i++;
  39. }
  40. else
  41. {
  42. swap(nums[--right], nums[i]);
  43. }
  44. }
  45. return { left,right };
  46. }
  47. };

3. 数组中的第K个最大元素(中等)

快速选择算法是基于快速排序算法思想的用于解决Top-K问题的算法,时间复杂度为O(n)。

在快排中,当我们把数组分成三块之后:[begin, left],[left + 1, right - 1],[right, end],我们可以通过计算每一个区间内元素的个数,进而推断出我们要找的元素是在哪一个区间里面。

  1. class Solution {
  2. public:
  3. int findKthLargest(vector<int>& nums, int k) {
  4. srand((unsigned int)time(nullptr)); // 设置随机数种子
  5. return quickSelect(nums, 0, nums.size() - 1, k);
  6. }
  7. private:
  8. int quickSelect(vector<int>& nums, int begin, int end, int k)
  9. {
  10. // 递归出口
  11. if (begin == end)
  12. return nums[begin];
  13. // 划分左中右三个子区间
  14. vector<int> ret = partition(nums, begin, end);
  15. int left = ret[0];
  16. int right = ret[1];
  17. int key = ret[2];
  18. // 分类讨论
  19. int c = end - right + 1;
  20. int b = right - left - 1;
  21. if (c >= k)
  22. return quickSelect(nums, right, end, k);
  23. else if (b + c >= k)
  24. return key;
  25. else
  26. return quickSelect(nums, begin, left, k - b - c);
  27. }
  28. vector<int> partition(vector<int>& nums, int begin, int end)
  29. {
  30. // 随机取key
  31. int key = nums[rand() % (end - begin + 1) + begin];
  32. // 数组分三块
  33. int left = begin - 1;
  34. int right = end + 1;
  35. int i = begin;
  36. while (i < right)
  37. {
  38. if (nums[i] < key)
  39. {
  40. swap(nums[++left], nums[i++]);
  41. }
  42. else if (nums[i] == key)
  43. {
  44. i++;
  45. }
  46. else
  47. {
  48. swap(nums[--right], nums[i]);
  49. }
  50. }
  51. return { left,right,key };
  52. }
  53. };

4. 最小K个数(中等)

和上一题“数组中的第K个最大元素”类似。

  1. class Solution {
  2. public:
  3. vector<int> smallestK(vector<int>& arr, int k) {
  4. srand((unsigned int)time(nullptr)); // 设置随机数种子
  5. quickSelect(arr, 0, arr.size() - 1, k);
  6. return { arr.begin(),arr.begin() + k };
  7. }
  8. private:
  9. void quickSelect(vector<int>& nums, int begin, int end, int k)
  10. {
  11. // 递归出口
  12. if (begin >= end)
  13. return;
  14. // 划分左中右三个子区间
  15. vector<int> ret = partition(nums, begin, end);
  16. int left = ret[0];
  17. int right = ret[1];
  18. // 分类讨论
  19. int a = left - begin + 1;
  20. int b = right - left - 1;
  21. if (a > k)
  22. quickSelect(nums, begin, left, k);
  23. else if (a + b >= k)
  24. return;
  25. else
  26. quickSelect(nums, right, end, k - a - b);
  27. }
  28. vector<int> partition(vector<int>& nums, int begin, int end)
  29. {
  30. // 随机取key
  31. int key = nums[rand() % (end - begin + 1) + begin];
  32. // 数组分三块
  33. int left = begin - 1;
  34. int right = end + 1;
  35. int i = begin;
  36. while (i < right)
  37. {
  38. if (nums[i] < key)
  39. {
  40. swap(nums[++left], nums[i++]);
  41. }
  42. else if (nums[i] == key)
  43. {
  44. i++;
  45. }
  46. else
  47. {
  48. swap(nums[--right], nums[i]);
  49. }
  50. }
  51. return { left,right };
  52. }
  53. };

归并排序:

1. 排序数组(中等)

归并排序的基本思想是基于分治法的:将两个有序表合并成一个新的有序表,这种两两归并的排序方法称为2路归并排序。

  1. class Solution {
  2. public:
  3. vector<int> sortArray(vector<int>& nums) {
  4. mergeSort(nums, 0, nums.size() - 1);
  5. return nums;
  6. }
  7. private:
  8. void mergeSort(vector<int>& nums, int begin, int end)
  9. {
  10. // 递归出口
  11. if (begin >= end)
  12. return;
  13. // 划分左右两个子区间
  14. int mid = (begin + end) / 2;
  15. // 递归排序两个子区间
  16. mergeSort(nums, begin, mid);
  17. mergeSort(nums, mid + 1, end);
  18. // 合并两个有序子区间
  19. merge(nums, begin, mid, end);
  20. }
  21. void merge(vector<int>& nums, int begin, int mid, int end)
  22. {
  23. int n = end - begin + 1;
  24. vector<int> tmp(n);
  25. int begin1 = begin, end1 = mid;
  26. int begin2 = mid + 1, end2 = end;
  27. int i = 0;
  28. // 升序排序
  29. while (begin1 <= end1 && begin2 <= end2)
  30. {
  31. if (nums[begin1] < nums[begin2])
  32. {
  33. tmp[i++] = nums[begin1++];
  34. }
  35. else
  36. {
  37. tmp[i++] = nums[begin2++];
  38. }
  39. }
  40. while (begin1 <= end1) // 左区间有剩余
  41. {
  42. tmp[i++] = nums[begin1++];
  43. }
  44. while (begin2 <= end2) // 右区间有剩余
  45. {
  46. tmp[i++] = nums[begin2++];
  47. }
  48. // 拷贝
  49. for (int i = 0; i < n; i++)
  50. {
  51. nums[i + begin] = tmp[i];
  52. }
  53. }
  54. };

2. 交易逆序对的总数(困难)

如果我们将数组划分为左右两个区间,那么逆序对的选择有3种情况,3种情况下逆序对的总和就是整个数组逆序对的总数。

逆序对中两个元素:

  • 全部从左区间中选择
  • 全部从右区间中选择
  • 一个从左区间中选择,一个从右区间中选择

思路恰好对应了归并排序:递归排序两个子区间,合并两个有序子区间。

我们可以结合归并排序,求出逆序对的数量:

  • 求出左区间中逆序对的数量,并对区间排序
  • 求出右区间中逆序对的数量,并对区间排序
  • 求出“一个从左区间中选择,一个从右区间中选择”的逆序对的数量,并合并两个有序子区间

那么,“一个从左区间中选择,一个从右区间中选择”的逆序对的数量,如何求?

方法一:针对右区间的某数,统计左区间中有多少数比它大。

升序排序的情况:

处理剩余元素:

  • 如果左区间有剩余,剩余元素都> 右区间的所有元素,但是它们都已经被统计过了,不会再产生逆序对,直接把剩余元素接到tmp数组后面即可。
  • 如果右区间有剩余,剩余元素都>= 左区间的所有元素,不会产生逆序对,直接把剩余元素接到tmp数组后面即可。

降序排序的情况:

处理剩余元素:

  • 如果左区间有剩余,剩余元素都<= 右区间的所有元素,不会产生逆序对,直接把剩余元素接到tmp数组后面即可。
  • 如果右区间有剩余,剩余元素都< 左区间的所有元素,但是它们都没有被统计过,循环把end1 - begin1 + 1添加到答案并把剩余元素逐个接到tmp数组后面。

显然,方法一用升序排序比降序排序简单,因为不用特殊处理剩余元素。

升序排序版:

  1. class Solution {
  2. public:
  3. int reversePairs(vector<int>& record) {
  4. return mergeSort(record, 0, record.size() - 1);
  5. }
  6. private:
  7. int mergeSort(vector<int>& nums, int begin, int end)
  8. {
  9. // 递归出口
  10. if (begin >= end)
  11. return 0;
  12. // 划分左右两个子区间
  13. int mid = (begin + end) / 2;
  14. int ans = 0;
  15. // 分别将左右两个区间的逆序对的数量添加进答案中
  16. ans += mergeSort(nums, begin, mid);
  17. ans += mergeSort(nums, mid + 1, end);
  18. // 将“一个从左区间中选择,一个从右区间中选择”的逆序对的数量添加进答案中
  19. ans += merge(nums, begin, mid, end);
  20. return ans;
  21. }
  22. // 求出“一个从左区间中选择,一个从右区间中选择”的逆序对的数量,并合并两个有序子区间
  23. int merge(vector<int>& nums, int begin, int mid, int end)
  24. {
  25. int n = end - begin + 1;
  26. vector<int> tmp(n);
  27. int begin1 = begin, end1 = mid;
  28. int begin2 = mid + 1, end2 = end;
  29. int i = 0;
  30. int ans = 0;
  31. // 升序排序
  32. int j1 = begin1, j2 = begin2;
  33. while (j1 <= end1 && j2 <= end2)
  34. {
  35. if (nums[j1] <= nums[j2])
  36. {
  37. tmp[i++] = nums[j1++];
  38. }
  39. else
  40. {
  41. ans += end1 - j1 + 1;
  42. tmp[i++] = nums[j2++];
  43. }
  44. }
  45. while (j1 <= end1) // 左区间有剩余
  46. {
  47. tmp[i++] = nums[j1++];
  48. }
  49. while (j2 <= end2) // 右区间有剩余
  50. {
  51. tmp[i++] = nums[j2++];
  52. }
  53. // 拷贝
  54. for (int i = 0; i < n; i++)
  55. {
  56. nums[i + begin] = tmp[i];
  57. }
  58. return ans;
  59. }
  60. };

降序排序版:

  1. class Solution {
  2. public:
  3. int reversePairs(vector<int>& record) {
  4. return mergeSort(record, 0, record.size() - 1);
  5. }
  6. private:
  7. int mergeSort(vector<int>& nums, int begin, int end)
  8. {
  9. // 递归出口
  10. if (begin >= end)
  11. return 0;
  12. // 划分左右两个子区间
  13. int mid = (begin + end) / 2;
  14. int ans = 0;
  15. // 分别将左右两个区间的逆序对的数量添加进答案中
  16. ans += mergeSort(nums, begin, mid);
  17. ans += mergeSort(nums, mid + 1, end);
  18. // 将“一个从左区间中选择,一个从右区间中选择”的逆序对的数量添加进答案中
  19. ans += merge(nums, begin, mid, end);
  20. return ans;
  21. }
  22. // 求出“一个从左区间中选择,一个从右区间中选择”的逆序对的数量,并合并两个有序子区间
  23. int merge(vector<int>& nums, int begin, int mid, int end)
  24. {
  25. int n = end - begin + 1;
  26. vector<int> tmp(n);
  27. int begin1 = begin, end1 = mid;
  28. int begin2 = mid + 1, end2 = end;
  29. int i = 0;
  30. int ans = 0;
  31. // 降序排序
  32. int j1 = begin1, j2 = begin2;
  33. while (j1 <= end1 && j2 <= end2)
  34. {
  35. if (nums[j1] > nums[j2])
  36. {
  37. tmp[i++] = nums[j1++];
  38. }
  39. else
  40. {
  41. ans += j1 - begin1;
  42. tmp[i++] = nums[j2++];
  43. }
  44. }
  45. while (j1 <= end1) // 左区间有剩余
  46. {
  47. tmp[i++] = nums[j1++];
  48. }
  49. while (j2 <= end2) // 右区间有剩余
  50. {
  51. ans += end1 - begin1 + 1;
  52. tmp[i++] = nums[j2++];
  53. }
  54. // 拷贝
  55. for (int i = 0; i < n; i++)
  56. {
  57. nums[i + begin] = tmp[i];
  58. }
  59. return ans;
  60. }
  61. };

方法二:针对左区间的某数,统计右区间中有多少数比它小。

升序排序的情况:

处理剩余元素:

  • 如果左区间有剩余,剩余元素都> 右区间的所有元素,但是它们都没有被统计过,循环把end2 - begin2 + 1添加到答案并把剩余元素逐个接到tmp数组后面。
  • 如果右区间有剩余,剩余元素都>= 左区间的所有元素,不会产生逆序对,直接把剩余元素接到tmp数组后面即可。

降序排序的情况:

处理剩余元素:

  • 如果左区间有剩余,剩余元素都<= 右区间的所有元素,不会产生逆序对,直接把剩余元素接到tmp数组后面即可。
  • 如果右区间有剩余,剩余元素都< 左区间的所有元素,但是它们都已经被统计过了,不会再产生逆序对,直接把剩余元素接到tmp数组后面即可。

显然,方法二用降序排序比升序排序简单,因为不用特殊处理剩余元素。

升序排序版:

  1. class Solution {
  2. public:
  3. int reversePairs(vector<int>& record) {
  4. return mergeSort(record, 0, record.size() - 1);
  5. }
  6. private:
  7. int mergeSort(vector<int>& nums, int begin, int end)
  8. {
  9. // 递归出口
  10. if (begin >= end)
  11. return 0;
  12. // 划分左右两个子区间
  13. int mid = (begin + end) / 2;
  14. int ans = 0;
  15. // 分别将左右两个区间的逆序对的数量添加进答案中
  16. ans += mergeSort(nums, begin, mid);
  17. ans += mergeSort(nums, mid + 1, end);
  18. // 将“一个从左区间中选择,一个从右区间中选择”的逆序对的数量添加进答案中
  19. ans += merge(nums, begin, mid, end);
  20. return ans;
  21. }
  22. // 求出“一个从左区间中选择,一个从右区间中选择”的逆序对的数量,并合并两个有序子区间
  23. int merge(vector<int>& nums, int begin, int mid, int end)
  24. {
  25. int n = end - begin + 1;
  26. vector<int> tmp(n);
  27. int begin1 = begin, end1 = mid;
  28. int begin2 = mid + 1, end2 = end;
  29. int i = 0;
  30. int ans = 0;
  31. // 升序排序
  32. int j1 = begin1, j2 = begin2;
  33. while (j1 <= end1 && j2 <= end2)
  34. {
  35. if (nums[j2] < nums[j1])
  36. {
  37. tmp[i++] = nums[j2++];
  38. }
  39. else
  40. {
  41. ans += j2 - begin2;
  42. tmp[i++] = nums[j1++];
  43. }
  44. }
  45. while (j1 <= end1) // 左区间有剩余
  46. {
  47. ans += end2 - begin2 + 1;
  48. tmp[i++] = nums[j1++];
  49. }
  50. while (j2 <= end2) // 右区间有剩余
  51. {
  52. tmp[i++] = nums[j2++];
  53. }
  54. // 拷贝
  55. for (int i = 0; i < n; i++)
  56. {
  57. nums[i + begin] = tmp[i];
  58. }
  59. return ans;
  60. }
  61. };

降序排序版:

  1. class Solution {
  2. public:
  3. int reversePairs(vector<int>& record) {
  4. return mergeSort(record, 0, record.size() - 1);
  5. }
  6. private:
  7. int mergeSort(vector<int>& nums, int begin, int end)
  8. {
  9. // 递归出口
  10. if (begin >= end)
  11. return 0;
  12. // 划分左右两个子区间
  13. int mid = (begin + end) / 2;
  14. int ans = 0;
  15. // 分别将左右两个区间的逆序对的数量添加进答案中
  16. ans += mergeSort(nums, begin, mid);
  17. ans += mergeSort(nums, mid + 1, end);
  18. // 将“一个从左区间中选择,一个从右区间中选择”的逆序对的数量添加进答案中
  19. ans += merge(nums, begin, mid, end);
  20. return ans;
  21. }
  22. // 求出“一个从左区间中选择,一个从右区间中选择”的逆序对的数量,并合并两个有序子区间
  23. int merge(vector<int>& nums, int begin, int mid, int end)
  24. {
  25. int n = end - begin + 1;
  26. vector<int> tmp(n);
  27. int begin1 = begin, end1 = mid;
  28. int begin2 = mid + 1, end2 = end;
  29. int i = 0;
  30. int ans = 0;
  31. // 降序排序
  32. int j1 = begin1, j2 = begin2;
  33. while (j1 <= end1 && j2 <= end2)
  34. {
  35. if (nums[j2] >= nums[j1])
  36. {
  37. tmp[i++] = nums[j2++];
  38. }
  39. else
  40. {
  41. ans += end2 - j2 + 1;
  42. tmp[i++] = nums[j1++];
  43. }
  44. }
  45. while (j1 <= end1) // 左区间有剩余
  46. {
  47. tmp[i++] = nums[j1++];
  48. }
  49. while (j2 <= end2) // 右区间有剩余
  50. {
  51. tmp[i++] = nums[j2++];
  52. }
  53. // 拷贝
  54. for (int i = 0; i < n; i++)
  55. {
  56. nums[i + begin] = tmp[i];
  57. }
  58. return ans;
  59. }
  60. };

3. 计算右侧小于当前元素的个数(困难)

本题是上一题“逆序对”方法二——“针对左区间的某数,统计右区间中有多少数比它小”的拓展。

根据逆序对的经验,方法二用降序排序比升序排序简单,因为不用特殊处理剩余元素。

需要一个索引数组将数组元素和对应的下标绑定在一起。

  1. class Solution {
  2. public:
  3. vector<int> countSmaller(vector<int>& nums) {
  4. int n = nums.size();
  5. ans.resize(n);
  6. index.resize(n);
  7. // 初始化索引数组
  8. for (int i = 0; i < n; i++)
  9. {
  10. index[i] = i;
  11. }
  12. mergeSort(nums, 0, n - 1);
  13. return ans;
  14. }
  15. private:
  16. void mergeSort(vector<int>& nums, int begin, int end)
  17. {
  18. // 递归出口
  19. if (begin >= end)
  20. return;
  21. // 划分左右两个子区间
  22. int mid = (begin + end) / 2;
  23. // 递归处理左右两个区间
  24. mergeSort(nums, begin, mid);
  25. mergeSort(nums, mid + 1, end);
  26. // 处理“针对左区间的某数,统计右区间中有多少数比它小”
  27. merge(nums, begin, mid, end);
  28. return;
  29. }
  30. // 针对左区间的某数,统计右区间中有多少数比它小,并合并两个有序子区间
  31. void merge(vector<int>& nums, int begin, int mid, int end)
  32. {
  33. int n = end - begin + 1;
  34. vector<int> tmpNums(n); // 排序用的辅助数组
  35. vector<int> tmpIndex(n); // 处理下标用的辅助数组
  36. int begin1 = begin, end1 = mid;
  37. int begin2 = mid + 1, end2 = end;
  38. int i = 0;
  39. // 降序排序
  40. int j1 = begin1, j2 = begin2;
  41. while (j1 <= end1 && j2 <= end2)
  42. {
  43. if (nums[j2] >= nums[j1])
  44. {
  45. tmpNums[i] = nums[j2];
  46. tmpIndex[i++] = index[j2++];
  47. }
  48. else
  49. {
  50. ans[index[j1]] += end2 - j2 + 1;
  51. tmpNums[i] = nums[j1];
  52. tmpIndex[i++] = index[j1++];
  53. }
  54. }
  55. while (j1 <= end1) // 左区间有剩余
  56. {
  57. tmpNums[i] = nums[j1];
  58. tmpIndex[i++] = index[j1++];
  59. }
  60. while (j2 <= end2) // 右区间有剩余
  61. {
  62. tmpNums[i] = nums[j2];
  63. tmpIndex[i++] = index[j2++];
  64. }
  65. // 拷贝
  66. for (int i = 0; i < n; i++)
  67. {
  68. nums[i + begin] = tmpNums[i];
  69. index[i + begin] = tmpIndex[i];
  70. }
  71. }
  72. vector<int> ans;
  73. vector<int> index; // 索引数组,index[j]保存当前nums[j]的原始下标
  74. };

4. 翻转对(困难)

和逆序对类似,所以也可以用归并排序的思想解决问题。

如果我们将数组划分为左右两个区间,那么翻转对的选择有3种情况,3种情况下翻转对的总和就是整个数组翻转对的总数。

翻转对中两个元素:

  • 全部从左区间中选择
  • 全部从右区间中选择
  • 一个从左区间中选择,一个从右区间中选择

和逆序对不同的是,逆序对是在合并有序区间过程中,计算出翻转对的数量,对于翻转对,我们要先计算出翻转对的数量,再合并。

“一个从左区间中选择,一个从右区间中选择”的翻转对的数量,如何求?

方法一:针对右区间的某数,统计左区间中有多少数比它的2倍大。

根据逆序对的经验,方法一用升序排序比降序排序简单,因为不用特殊处理剩余元素。

  1. class Solution {
  2. public:
  3. int reversePairs(vector<int>& nums) {
  4. return mergeSort(nums, 0, nums.size() - 1);
  5. }
  6. private:
  7. int mergeSort(vector<int>& nums, int begin, int end)
  8. {
  9. // 递归出口
  10. if (begin >= end)
  11. return 0;
  12. // 划分左右两个子区间
  13. int mid = (begin + end) / 2;
  14. int ans = 0;
  15. // 分别将左右两个区间的翻转对的数量添加进答案中
  16. ans += mergeSort(nums, begin, mid);
  17. ans += mergeSort(nums, mid + 1, end);
  18. // 将“一个从左区间中选择,一个从右区间中选择”的翻转对的数量添加进答案中
  19. ans += merge(nums, begin, mid, end);
  20. return ans;
  21. }
  22. // 求出“一个从左区间中选择,一个从右区间中选择”的翻转对的数量,并合并两个有序子区间
  23. int merge(vector<int>& nums, int begin, int mid, int end)
  24. {
  25. int begin1 = begin, end1 = mid;
  26. int begin2 = mid + 1, end2 = end;
  27. int ans = 0;
  28. // 升序排序的情况下翻转对的数量
  29. int j1 = begin1, j2 = begin2;
  30. while (j1 <= end1 && j2 <= end2)
  31. {
  32. while (j1 <= end1 && nums[j1] / 2.0 <= nums[j2]) // 防止2 * nums[j2]溢出
  33. {
  34. j1++;
  35. }
  36. if (j1 > end1)
  37. break;
  38. ans += end1 - j1 + 1;
  39. j2++;
  40. }
  41. // 合并升序排序的两个区间
  42. int n = end - begin + 1;
  43. vector<int> tmp(n);
  44. int i = 0;
  45. j1 = begin1;
  46. j2 = begin2;
  47. while (j1 <= end1 && j2 <= end2)
  48. {
  49. if (nums[j1] < nums[j2])
  50. {
  51. tmp[i++] = nums[j1++];
  52. }
  53. else
  54. {
  55. tmp[i++] = nums[j2++];
  56. }
  57. }
  58. while (j1 <= end1) // 左区间有剩余
  59. {
  60. tmp[i++] = nums[j1++];
  61. }
  62. while (j2 <= end2) // 右区间有剩余
  63. {
  64. tmp[i++] = nums[j2++];
  65. }
  66. // 拷贝
  67. for (int i = 0; i < n; i++)
  68. {
  69. nums[i + begin] = tmp[i];
  70. }
  71. return ans;
  72. }
  73. };

方法二:针对左区间的某数,统计右区间中有多少数的2倍比它小。

根据逆序对的经验,方法二用降序排序比升序排序简单,因为不用特殊处理剩余元素。

  1. class Solution {
  2. public:
  3. int reversePairs(vector<int>& nums) {
  4. return mergeSort(nums, 0, nums.size() - 1);
  5. }
  6. private:
  7. int mergeSort(vector<int>& nums, int begin, int end)
  8. {
  9. // 递归出口
  10. if (begin >= end)
  11. return 0;
  12. // 划分左右两个子区间
  13. int mid = (begin + end) / 2;
  14. int ans = 0;
  15. // 分别将左右两个区间的翻转对的数量添加进答案中
  16. ans += mergeSort(nums, begin, mid);
  17. ans += mergeSort(nums, mid + 1, end);
  18. // 将“一个从左区间中选择,一个从右区间中选择”的翻转对的数量添加进答案中
  19. ans += merge(nums, begin, mid, end);
  20. return ans;
  21. }
  22. // 求出“一个从左区间中选择,一个从右区间中选择”的翻转对的数量,并合并两个有序子区间
  23. int merge(vector<int>& nums, int begin, int mid, int end)
  24. {
  25. int begin1 = begin, end1 = mid;
  26. int begin2 = mid + 1, end2 = end;
  27. int ans = 0;
  28. // 降序排序的情况下翻转对的数量
  29. int j1 = begin1, j2 = begin2;
  30. while (j1 <= end1 && j2 <= end2)
  31. {
  32. while (j2 <= end2 && nums[j2] >= nums[j1] / 2.0) // 防止2 * nums[j2]溢出
  33. {
  34. j2++;
  35. }
  36. if (j2 > end2)
  37. break;
  38. ans += end2 - j2 + 1;
  39. j1++;
  40. }
  41. // 合并降序排序的两个区间
  42. int n = end - begin + 1;
  43. vector<int> tmp(n);
  44. int i = 0;
  45. j1 = begin1;
  46. j2 = begin2;
  47. while (j1 <= end1 && j2 <= end2)
  48. {
  49. if (nums[j1] > nums[j2])
  50. {
  51. tmp[i++] = nums[j1++];
  52. }
  53. else
  54. {
  55. tmp[i++] = nums[j2++];
  56. }
  57. }
  58. while (j1 <= end1) // 左区间有剩余
  59. {
  60. tmp[i++] = nums[j1++];
  61. }
  62. while (j2 <= end2) // 右区间有剩余
  63. {
  64. tmp[i++] = nums[j2++];
  65. }
  66. // 拷贝
  67. for (int i = 0; i < n; i++)
  68. {
  69. nums[i + begin] = tmp[i];
  70. }
  71. return ans;
  72. }
  73. };

5. 排序链表(中等)

分治的思想,类似归并排序:

  1. 划分两个子链表

  2. 分别对两个子链表进行排序,形成两个有序链表

  3. 合并两个有序链表

重复的子问题——函数头设计

ListNode* sortList(ListNode* head)

子问题在做什么——函数体设计

  1. 划分两个子链表,找中间节点:ListNode* mid = midNode(head);
  2. 递归排序右子链表:ListNode* l1 = sortList(mid->next);
  3. 断开两个子链表:mid->next = nullptr;
  4. 递归排序左子链表:ListNode* l2 = sortList(head);
  5. 合并两个有序链表:return mergeTowList(l1, l2);

递归出口

当链表只有一个节点时,不合并。另外,当题目给出空链表时,不合并。

  1. class Solution {
  2. public:
  3. ListNode* sortList(ListNode* head) {
  4. if (head == nullptr || head->next == nullptr)
  5. return head;
  6. ListNode* mid = midNode(head); // 中间节点
  7. ListNode* l1 = sortList(mid->next); // 排序右子链表
  8. mid->next = nullptr; // 断开两个子链表
  9. ListNode* l2 = sortList(head); // 排序左子链表
  10. return mergeTwoLists(l1, l2); // 合并两个有序链表
  11. }
  12. private:
  13. // 快慢指针找链表的中间节点,如果节点个数为偶数,取靠左的
  14. ListNode* midNode(ListNode* head)
  15. {
  16. ListNode* fast = head;
  17. ListNode* slow = head;
  18. // 慢指针每次走1步,快指针每次走2步
  19. // 如果节点个数为奇数,当快指针指向最后一个节点时,慢指针指向中间节点
  20. // 如果节点个数为奇数,当快指针指向倒数第二个节点时,慢指针指向靠左的中间节点
  21. while (fast->next && fast->next->next)
  22. {
  23. fast = fast->next->next;
  24. slow = slow->next;
  25. }
  26. return slow;
  27. }
  28. // 合并两个有序链表
  29. ListNode* mergeTwoLists(ListNode* list1, ListNode* list2)
  30. {
  31. ListNode* preHead = new ListNode; // 哨兵节点
  32. ListNode* tail = preHead;
  33. // 取小的尾插
  34. while (list1 && list2)
  35. {
  36. if (list1->val < list2->val)
  37. {
  38. tail->next = list1;
  39. tail = tail->next;
  40. list1 = list1->next;
  41. }
  42. else
  43. {
  44. tail->next = list2;
  45. tail = tail->next;
  46. list2 = list2->next;
  47. }
  48. }
  49. if (list1)
  50. {
  51. tail->next = list1;
  52. }
  53. if (list2)
  54. {
  55. tail->next = list2;
  56. }
  57. return preHead->next;
  58. }
  59. };

6. 合并 K 个升序链表(困难)

6.1 递归解法(归并)

分治的思想,类似归并排序:

  1. 划分两个子区间

  2. 分别对两个子区间的链表进行合并,形成两个有序链表

  3. 合并两个有序链表

重复的子问题——函数头设计

ListNode* merge(vector<ListNode*>& lists, int begin, int end)

子问题在做什么——函数体设计

  1. 划分两个子区间:int mid = (begin + end) / 2;
  2. 递归合并两个子区间:
    ListNode* l1 = merge(lists, begin, mid);
    ListNode* l2 = merge(lists, mid + 1, end);
  3. 合并两个有序链表:return mergeTowList(l1, l2);

递归出口

当区间只有一个链表时,不合并。另外,当题目给出空链表时,不合并。

  1. class Solution {
  2. public:
  3. ListNode* mergeKLists(vector<ListNode*>& lists) {
  4. return merge(lists, 0, lists.size() - 1);
  5. }
  6. private:
  7. ListNode* merge(vector<ListNode*>& lists, int begin, int end)
  8. {
  9. if (begin > end)
  10. return nullptr;
  11. if (begin == end)
  12. return lists[begin];
  13. int mid = (begin + end) / 2;
  14. ListNode* l1 = merge(lists, begin, mid);
  15. ListNode* l2 = merge(lists, mid + 1, end);
  16. return mergeTwoLists(l1, l2);
  17. }
  18. ListNode* mergeTwoLists(ListNode* list1, ListNode* list2)
  19. {
  20. ListNode* preHead = new ListNode; // 哨兵节点
  21. ListNode* tail = preHead;
  22. // 取小的尾插
  23. while (list1 && list2)
  24. {
  25. if (list1->val < list2->val)
  26. {
  27. tail->next = list1;
  28. tail = tail->next;
  29. list1 = list1->next;
  30. }
  31. else
  32. {
  33. tail->next = list2;
  34. tail = tail->next;
  35. list2 = list2->next;
  36. }
  37. }
  38. if (list1)
  39. {
  40. tail->next = list1;
  41. }
  42. if (list2)
  43. {
  44. tail->next = list2;
  45. }
  46. return preHead->next;
  47. }
  48. };

6.2 迭代解法(堆)

和“合并两个有序链表”类似,就是取小的尾插。怎么判断K个链表未合并的头节点中最小的那个?利用堆这个数据结构即可。把K个链表未合并的头节点放进一个小根堆,堆顶就是最小的那个。

  1. class Solution {
  2. public:
  3. ListNode* mergeKLists(vector<ListNode*>& lists) {
  4. // 创建小根堆
  5. priority_queue<ListNode*, vector<ListNode*>, cmp> heap;
  6. // 将所有头节点放进小根堆
  7. for (auto& l : lists)
  8. {
  9. if (l)
  10. {
  11. heap.push(l);
  12. }
  13. }
  14. // 合并链表
  15. ListNode* preHead = new ListNode; // 哨兵节点
  16. ListNode* tail = preHead;
  17. while (!heap.empty())
  18. {
  19. // 取堆顶节点尾插
  20. tail->next = heap.top();
  21. heap.pop();
  22. tail = tail->next;
  23. // 将刚才合并的节点的下一个节点补充进堆
  24. if (tail->next)
  25. {
  26. heap.push(tail->next);
  27. }
  28. }
  29. return preHead->next;
  30. }
  31. private:
  32. struct cmp
  33. {
  34. bool operator()(ListNode* n1, ListNode* n2)
  35. {
  36. return n1->val > n2->val;
  37. }
  38. };
  39. };
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/编程挑战者/article/detail/61196
推荐阅读
相关标签
  

闽ICP备14008679号