【LeetCode】排序精选12题

目录

排序:

1. 合并区间(中等)

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

快速排序:

1. 颜色分类(中等)

2. 排序数组(中等)

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

4. 最小K个数(中等)

归并排序:

1. 排序数组(中等)

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

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

4. 翻转对(困难)

5. 排序链表(中等)

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

6.1 递归解法(归并)

6.2 迭代解法(堆)


排序:

1. 合并区间(中等)

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

class Solution {
public:vector<vector<int>> merge(vector<vector<int>>& intervals) {int n = intervals.size();sort(intervals.begin(), intervals.end());vector<vector<int>> ans;for (int i = 1; i < n; i++){if (intervals[i][0] <= intervals[i - 1][1]){// 合并intervals[i][0] = min(intervals[i][0], intervals[i - 1][0]);intervals[i][1] = max(intervals[i][1], intervals[i - 1][1]);}else{ans.push_back(intervals[i - 1]);}}ans.push_back(intervals[n - 1]);return ans;}
};

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

对于arr1的两个元素:

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

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

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

class Solution {
public:vector<int> relativeSortArray(vector<int>& arr1, vector<int>& arr2) {unordered_map<int, int> hash;// 初始化哈希表for (int i = 0; i < arr2.size(); i++){hash[arr2[i]] = i;}// 自定义排序sort(arr1.begin(), arr1.end(), [&](int e1, int e2){if (hash.count(e1) && hash.count(e2))return hash[e1] < hash[e2];else if (hash.count(e1))return true;else if (hash.count(e2))return false;elsereturn e1 < e2;});return arr1;}
};

快速排序:

1. 颜色分类(中等)

类比移动零:

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

  • left指向0序列的最后一个,因此初始化为-1
  • i用来扫描数组,因此初始化为0
  • right指向2序列的第一个,因此初始化为n
class Solution {
public:void sortColors(vector<int>& nums) {int n = nums.size();int left = -1;int right = n;int i = 0;while (i < right){if (nums[i] == 0){swap(nums[++left], nums[i++]);}else if (nums[i] == 1){i++;}else{swap(nums[--right], nums[i]);}}}
};

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]上,这个过程称为一次划分。然后分别递归地对两个没排好序的子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。

class Solution {
public:vector<int> sortArray(vector<int>& nums) {srand((unsigned int)time(nullptr)); // 设置随机数种子quickSort(nums, 0, nums.size() - 1);return nums;}private:void quickSort(vector<int>& nums, int begin, int end){// 递归出口if (begin >= end)return;// 划分左中右三个子区间vector<int> ret = partition(nums, begin, end);int left = ret[0];int right = ret[1]; // 递归排序左右两个子区间quickSort(nums, begin, left);quickSort(nums, right, end);}vector<int> partition(vector<int>& nums, int begin, int end){// 随机取keyint key = nums[rand() % (end - begin + 1) + begin];// 数组分三块int left = begin - 1;int right = end + 1;int i = begin;while (i < right){if (nums[i] < key){swap(nums[++left], nums[i++]);}else if (nums[i] == key){i++;}else{swap(nums[--right], nums[i]);}}return { left,right };}
};

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

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

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

class Solution {
public:int findKthLargest(vector<int>& nums, int k) {srand((unsigned int)time(nullptr)); // 设置随机数种子return quickSelect(nums, 0, nums.size() - 1, k);}private:int quickSelect(vector<int>& nums, int begin, int end, int k){// 递归出口if (begin == end)return nums[begin];// 划分左中右三个子区间vector<int> ret = partition(nums, begin, end);int left = ret[0];int right = ret[1]; int key = ret[2];// 分类讨论int c = end - right + 1;int b = right - left - 1;if (c >= k)return quickSelect(nums, right, end, k);else if (b + c >= k)return key;elsereturn quickSelect(nums, begin, left, k - b - c);}vector<int> partition(vector<int>& nums, int begin, int end){// 随机取keyint key = nums[rand() % (end - begin + 1) + begin];// 数组分三块int left = begin - 1;int right = end + 1;int i = begin;while (i < right){if (nums[i] < key){swap(nums[++left], nums[i++]);}else if (nums[i] == key){i++;}else{swap(nums[--right], nums[i]);}}return { left,right,key };}
};

4. 最小K个数(中等)

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

class Solution {
public:vector<int> smallestK(vector<int>& arr, int k) {srand((unsigned int)time(nullptr)); // 设置随机数种子quickSelect(arr, 0, arr.size() - 1, k);return { arr.begin(),arr.begin() + k };}private:void quickSelect(vector<int>& nums, int begin, int end, int k){// 递归出口if (begin >= end)return;// 划分左中右三个子区间vector<int> ret = partition(nums, begin, end);int left = ret[0];int right = ret[1]; // 分类讨论int a = left - begin + 1;int b = right - left - 1;if (a > k)quickSelect(nums, begin, left, k);else if (a + b >= k)return;elsequickSelect(nums, right, end, k - a - b);}vector<int> partition(vector<int>& nums, int begin, int end){// 随机取keyint key = nums[rand() % (end - begin + 1) + begin];// 数组分三块int left = begin - 1;int right = end + 1;int i = begin;while (i < right){if (nums[i] < key){swap(nums[++left], nums[i++]);}else if (nums[i] == key){i++;}else{swap(nums[--right], nums[i]);}}return { left,right };}
};

归并排序:

1. 排序数组(中等)

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

class Solution {
public:vector<int> sortArray(vector<int>& nums) {mergeSort(nums, 0, nums.size() - 1);return nums;}private:void mergeSort(vector<int>& nums, int begin, int end){// 递归出口if (begin >= end)return;// 划分左右两个子区间int mid = (begin + end) / 2;// 递归排序两个子区间mergeSort(nums, begin, mid);mergeSort(nums, mid + 1, end);// 合并两个有序子区间merge(nums, begin, mid, end);}void merge(vector<int>& nums, int begin, int mid, int end){int n = end - begin + 1;vector<int> tmp(n);int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = 0;// 升序排序while (begin1 <= end1 && begin2 <= end2){if (nums[begin1] < nums[begin2]){tmp[i++] = nums[begin1++];}else{tmp[i++] = nums[begin2++];}}while (begin1 <= end1) // 左区间有剩余{tmp[i++] = nums[begin1++];}while (begin2 <= end2) // 右区间有剩余{tmp[i++] = nums[begin2++];}// 拷贝for (int i = 0; i < n; i++){nums[i + begin] = tmp[i];}}
};

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

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

逆序对中两个元素:

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

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

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

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

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

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

升序排序的情况:

处理剩余元素:

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

降序排序的情况:

处理剩余元素:

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

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

升序排序版:

class Solution {
public:int reversePairs(vector<int>& record) {return mergeSort(record, 0, record.size() - 1);}private:int mergeSort(vector<int>& nums, int begin, int end){// 递归出口if (begin >= end)return 0;// 划分左右两个子区间int mid = (begin + end) / 2;int ans = 0;// 分别将左右两个区间的逆序对的数量添加进答案中ans += mergeSort(nums, begin, mid);ans += mergeSort(nums, mid + 1, end);// 将“一个从左区间中选择,一个从右区间中选择”的逆序对的数量添加进答案中ans += merge(nums, begin, mid, end);return ans;}// 求出“一个从左区间中选择,一个从右区间中选择”的逆序对的数量,并合并两个有序子区间int merge(vector<int>& nums, int begin, int mid, int end){int n = end - begin + 1;vector<int> tmp(n);int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = 0;int ans = 0;// 升序排序int j1 = begin1, j2 = begin2;while (j1 <= end1 && j2 <= end2){if (nums[j1] <= nums[j2]){tmp[i++] = nums[j1++];}else{ans += end1 - j1 + 1;tmp[i++] = nums[j2++];}}while (j1 <= end1) // 左区间有剩余{tmp[i++] = nums[j1++];}while (j2 <= end2) // 右区间有剩余{tmp[i++] = nums[j2++];}// 拷贝for (int i = 0; i < n; i++){nums[i + begin] = tmp[i];}return ans;}
};

降序排序版:

class Solution {
public:int reversePairs(vector<int>& record) {return mergeSort(record, 0, record.size() - 1);}private:int mergeSort(vector<int>& nums, int begin, int end){// 递归出口if (begin >= end)return 0;// 划分左右两个子区间int mid = (begin + end) / 2;int ans = 0;// 分别将左右两个区间的逆序对的数量添加进答案中ans += mergeSort(nums, begin, mid);ans += mergeSort(nums, mid + 1, end);// 将“一个从左区间中选择,一个从右区间中选择”的逆序对的数量添加进答案中ans += merge(nums, begin, mid, end);return ans;}// 求出“一个从左区间中选择,一个从右区间中选择”的逆序对的数量,并合并两个有序子区间int merge(vector<int>& nums, int begin, int mid, int end){int n = end - begin + 1;vector<int> tmp(n);int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = 0;int ans = 0;// 降序排序int j1 = begin1, j2 = begin2;while (j1 <= end1 && j2 <= end2){if (nums[j1] > nums[j2]){tmp[i++] = nums[j1++];}else{ans += j1 - begin1;tmp[i++] = nums[j2++];}}while (j1 <= end1) // 左区间有剩余{tmp[i++] = nums[j1++];}while (j2 <= end2) // 右区间有剩余{ans += end1 - begin1 + 1;tmp[i++] = nums[j2++];}// 拷贝for (int i = 0; i < n; i++){nums[i + begin] = tmp[i];}return ans;}
};

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

升序排序的情况:

处理剩余元素:

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

降序排序的情况:

处理剩余元素:

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

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

升序排序版:

class Solution {
public:int reversePairs(vector<int>& record) {return mergeSort(record, 0, record.size() - 1);}private:int mergeSort(vector<int>& nums, int begin, int end){// 递归出口if (begin >= end)return 0;// 划分左右两个子区间int mid = (begin + end) / 2;int ans = 0;// 分别将左右两个区间的逆序对的数量添加进答案中ans += mergeSort(nums, begin, mid);ans += mergeSort(nums, mid + 1, end);// 将“一个从左区间中选择,一个从右区间中选择”的逆序对的数量添加进答案中ans += merge(nums, begin, mid, end);return ans;}// 求出“一个从左区间中选择,一个从右区间中选择”的逆序对的数量,并合并两个有序子区间int merge(vector<int>& nums, int begin, int mid, int end){int n = end - begin + 1;vector<int> tmp(n);int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = 0;int ans = 0;// 升序排序int j1 = begin1, j2 = begin2;while (j1 <= end1 && j2 <= end2){if (nums[j2] < nums[j1]){tmp[i++] = nums[j2++];}else{ans += j2 - begin2;tmp[i++] = nums[j1++];}}while (j1 <= end1) // 左区间有剩余{ans += end2 - begin2 + 1;tmp[i++] = nums[j1++];}while (j2 <= end2) // 右区间有剩余{tmp[i++] = nums[j2++];}// 拷贝for (int i = 0; i < n; i++){nums[i + begin] = tmp[i];}return ans;}
};

降序排序版:

class Solution {
public:int reversePairs(vector<int>& record) {return mergeSort(record, 0, record.size() - 1);}private:int mergeSort(vector<int>& nums, int begin, int end){// 递归出口if (begin >= end)return 0;// 划分左右两个子区间int mid = (begin + end) / 2;int ans = 0;// 分别将左右两个区间的逆序对的数量添加进答案中ans += mergeSort(nums, begin, mid);ans += mergeSort(nums, mid + 1, end);// 将“一个从左区间中选择,一个从右区间中选择”的逆序对的数量添加进答案中ans += merge(nums, begin, mid, end);return ans;}// 求出“一个从左区间中选择,一个从右区间中选择”的逆序对的数量,并合并两个有序子区间int merge(vector<int>& nums, int begin, int mid, int end){int n = end - begin + 1;vector<int> tmp(n);int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = 0;int ans = 0;// 降序排序int j1 = begin1, j2 = begin2;while (j1 <= end1 && j2 <= end2){if (nums[j2] >= nums[j1]){tmp[i++] = nums[j2++];}else{ans += end2 - j2 + 1;tmp[i++] = nums[j1++];}}while (j1 <= end1) // 左区间有剩余{tmp[i++] = nums[j1++];}while (j2 <= end2) // 右区间有剩余{tmp[i++] = nums[j2++];}// 拷贝for (int i = 0; i < n; i++){nums[i + begin] = tmp[i];}return ans;}
};

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

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

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

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

class Solution {
public:vector<int> countSmaller(vector<int>& nums) {int n = nums.size();ans.resize(n);index.resize(n);// 初始化索引数组for (int i = 0; i < n; i++){index[i]  = i;}mergeSort(nums, 0, n - 1);return ans;}private:void mergeSort(vector<int>& nums, int begin, int end){// 递归出口if (begin >= end)return;// 划分左右两个子区间int mid = (begin + end) / 2;// 递归处理左右两个区间mergeSort(nums, begin, mid);mergeSort(nums, mid + 1, end);// 处理“针对左区间的某数,统计右区间中有多少数比它小”merge(nums, begin, mid, end);return;}// 针对左区间的某数,统计右区间中有多少数比它小,并合并两个有序子区间void merge(vector<int>& nums, int begin, int mid, int end){int n = end - begin + 1;vector<int> tmpNums(n); // 排序用的辅助数组vector<int> tmpIndex(n); // 处理下标用的辅助数组int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = 0;// 降序排序int j1 = begin1, j2 = begin2;while (j1 <= end1 && j2 <= end2){if (nums[j2] >= nums[j1]){tmpNums[i] = nums[j2];tmpIndex[i++] = index[j2++];}else{ans[index[j1]] += end2 - j2 + 1;tmpNums[i] = nums[j1];tmpIndex[i++] = index[j1++];}}while (j1 <= end1) // 左区间有剩余{tmpNums[i] = nums[j1];tmpIndex[i++] = index[j1++];}while (j2 <= end2) // 右区间有剩余{tmpNums[i] = nums[j2];tmpIndex[i++] = index[j2++];}// 拷贝for (int i = 0; i < n; i++){nums[i + begin] = tmpNums[i];index[i + begin] = tmpIndex[i];}}vector<int> ans;vector<int> index; // 索引数组,index[j]保存当前nums[j]的原始下标
};

4. 翻转对(困难)

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

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

翻转对中两个元素:

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

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

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

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

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

class Solution {
public:int reversePairs(vector<int>& nums) {return mergeSort(nums, 0, nums.size() - 1);}private:int mergeSort(vector<int>& nums, int begin, int end){// 递归出口if (begin >= end)return 0;// 划分左右两个子区间int mid = (begin + end) / 2;int ans = 0;// 分别将左右两个区间的翻转对的数量添加进答案中ans += mergeSort(nums, begin, mid);ans += mergeSort(nums, mid + 1, end);// 将“一个从左区间中选择,一个从右区间中选择”的翻转对的数量添加进答案中ans += merge(nums, begin, mid, end);return ans;}// 求出“一个从左区间中选择,一个从右区间中选择”的翻转对的数量,并合并两个有序子区间int merge(vector<int>& nums, int begin, int mid, int end){int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int ans = 0;// 升序排序的情况下翻转对的数量int j1 = begin1, j2 = begin2;while (j1 <= end1 && j2 <= end2){while (j1 <= end1 && nums[j1] / 2.0 <= nums[j2]) // 防止2 * nums[j2]溢出{j1++;}if (j1 > end1)break;ans += end1 - j1 + 1;j2++;}// 合并升序排序的两个区间int n = end - begin + 1;vector<int> tmp(n);int i = 0;j1 = begin1;j2 = begin2;while (j1 <= end1 && j2 <= end2){if (nums[j1] < nums[j2]){tmp[i++] = nums[j1++];}else{tmp[i++] = nums[j2++];}}while (j1 <= end1) // 左区间有剩余{tmp[i++] = nums[j1++];}while (j2 <= end2) // 右区间有剩余{tmp[i++] = nums[j2++];}// 拷贝for (int i = 0; i < n; i++){nums[i + begin] = tmp[i];}return ans;}
};

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

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

class Solution {
public:int reversePairs(vector<int>& nums) {return mergeSort(nums, 0, nums.size() - 1);}private:int mergeSort(vector<int>& nums, int begin, int end){// 递归出口if (begin >= end)return 0;// 划分左右两个子区间int mid = (begin + end) / 2;int ans = 0;// 分别将左右两个区间的翻转对的数量添加进答案中ans += mergeSort(nums, begin, mid);ans += mergeSort(nums, mid + 1, end);// 将“一个从左区间中选择,一个从右区间中选择”的翻转对的数量添加进答案中ans += merge(nums, begin, mid, end);return ans;}// 求出“一个从左区间中选择,一个从右区间中选择”的翻转对的数量,并合并两个有序子区间int merge(vector<int>& nums, int begin, int mid, int end){int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int ans = 0;// 降序排序的情况下翻转对的数量int j1 = begin1, j2 = begin2;while (j1 <= end1 && j2 <= end2){while (j2 <= end2 && nums[j2] >= nums[j1] / 2.0) // 防止2 * nums[j2]溢出{j2++;}if (j2 > end2)break;ans += end2 - j2 + 1;j1++;}// 合并降序排序的两个区间int n = end - begin + 1;vector<int> tmp(n);int i = 0;j1 = begin1;j2 = begin2;while (j1 <= end1 && j2 <= end2){if (nums[j1] > nums[j2]){tmp[i++] = nums[j1++];}else{tmp[i++] = nums[j2++];}}while (j1 <= end1) // 左区间有剩余{tmp[i++] = nums[j1++];}while (j2 <= end2) // 右区间有剩余{tmp[i++] = nums[j2++];}// 拷贝for (int i = 0; i < n; i++){nums[i + begin] = tmp[i];}return ans;}
};

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

递归出口

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

class Solution {
public:ListNode* sortList(ListNode* head) {if (head == nullptr || head->next == nullptr)return head;ListNode* mid = midNode(head); // 中间节点ListNode* l1 = sortList(mid->next); // 排序右子链表mid->next = nullptr; // 断开两个子链表ListNode* l2 = sortList(head); // 排序左子链表return mergeTwoLists(l1, l2); // 合并两个有序链表}private:// 快慢指针找链表的中间节点,如果节点个数为偶数,取靠左的ListNode* midNode(ListNode* head){ListNode* fast = head;ListNode* slow = head;// 慢指针每次走1步,快指针每次走2步// 如果节点个数为奇数,当快指针指向最后一个节点时,慢指针指向中间节点// 如果节点个数为奇数,当快指针指向倒数第二个节点时,慢指针指向靠左的中间节点while (fast->next && fast->next->next){fast = fast->next->next;slow = slow->next;}return slow;}// 合并两个有序链表ListNode* mergeTwoLists(ListNode* list1, ListNode* list2){ListNode* preHead = new ListNode; // 哨兵节点ListNode* tail = preHead;// 取小的尾插while (list1 && list2){if (list1->val < list2->val){tail->next = list1;tail = tail->next;list1 = list1->next;}else{tail->next = list2;tail = tail->next;list2 = list2->next;}}if (list1){tail->next = list1;}if (list2){tail->next = list2;}return preHead->next;}
};

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

递归出口

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

class Solution {
public:ListNode* mergeKLists(vector<ListNode*>& lists) {return merge(lists, 0, lists.size() - 1);}private:ListNode* merge(vector<ListNode*>& lists, int begin, int end){if (begin > end)return nullptr;if (begin == end)return lists[begin];int mid = (begin + end) / 2;ListNode* l1 = merge(lists, begin, mid);ListNode* l2 = merge(lists, mid + 1, end);return mergeTwoLists(l1, l2);}ListNode* mergeTwoLists(ListNode* list1, ListNode* list2){ListNode* preHead = new ListNode; // 哨兵节点ListNode* tail = preHead;// 取小的尾插while (list1 && list2){if (list1->val < list2->val){tail->next = list1;tail = tail->next;list1 = list1->next;}else{tail->next = list2;tail = tail->next;list2 = list2->next;}}if (list1){tail->next = list1;}if (list2){tail->next = list2;}return preHead->next;}
};

6.2 迭代解法(堆)

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

class Solution {
public:ListNode* mergeKLists(vector<ListNode*>& lists) {// 创建小根堆priority_queue<ListNode*, vector<ListNode*>, cmp> heap;// 将所有头节点放进小根堆for (auto& l : lists){if (l){heap.push(l);}}// 合并链表ListNode* preHead = new ListNode; // 哨兵节点ListNode* tail = preHead;while (!heap.empty()){// 取堆顶节点尾插tail->next = heap.top();heap.pop();tail = tail->next;// 将刚才合并的节点的下一个节点补充进堆if (tail->next){heap.push(tail->next);}}return preHead->next;}private:struct cmp{bool operator()(ListNode* n1, ListNode* n2){return n1->val > n2->val;}};
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/248736.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HCIA-HarmonyOS设备开发认证-3.内核基础

目录 前言目标一、进程与线程待续。。。 前言 对于任何一个操作系统而言&#xff0c;内核的运行机制与原理是最为关键的部分。本章内容从多角度了解HarmonyOS的内核运行机制&#xff0c;涵盖进程与线程的概念&#xff0c;内存管理机制&#xff0c;网络特性&#xff0c;文件系统…

Arduino开发实例-DRV8833电机驱动器控制直流电机

DRV8833电机驱动器控制直流电机 文章目录 DRV8833电机驱动器控制直流电机1、DRV8833电机驱动器介绍2、硬件接线图3、代码实现DRV8833 使用 MOSFET,而不是 BJT。 MOSFET 的压降几乎可以忽略不计,这意味着几乎所有来自电源的电压都会传递到电机。 这就是为什么 DRV8833 不仅比基…

Excel中将16进制数转化成10进制(有/无符号)

Excel中将16进制数转化成10进制&#xff08;有/无符号&#xff09; Excel或者matlab中常用XXX2XXX进行不同进制的转换 16进制转10进制&#xff08;无符号数&#xff09;&#xff1a;HEX2DEC 16进制转10进制&#xff08;有符号数&#xff09;&#xff1a; FA46为例&#xff0c…

【ARM Trace32(劳特巴赫) 使用介绍 6.1 -- 外设寄存器查看与修改】

请阅读【Trace32 ARM 专栏导读】 文章目录 外设寄存器查看与修改寄存器值修改外设寄存器查看与修改 外设寄存器的查看与修改,离不开TRACE32的外设文件(*.per),per文件一般存在于TRACE32的安装根目录下。 一般情况下,在调试时,TRACE32会根据当前选择的芯片名自动选择合适的…

STM32+ESP8266 实现物联网设备节点

目录 一、硬件准备 二、编译环境 三、源代码地址 四、说明 五、测试方法 六、所有测试工具和文档 本项目使用stm32F103ZEesp8266实现一个物联网的通信节点&#xff0c;目前支持的协议有mqtt&#xff0c;tcp。后续会持续更新&#xff0c;增加JSON&#xff0c;传感器&#…

【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]

阅读导航 引言一、设计模式概念&#xff08;了解&#xff09;二、单例模式1. 饿汉模式&#xff08;1&#xff09;概念&#xff08;2&#xff09;模拟实现&#xff08;3&#xff09;优缺点&#xff08;4&#xff09;适用场景 2. 懒汉模式&#xff08;1&#xff09;概念&#xff…

SpringBoot 结合 liteflow 规则引擎使用

1、前言 在日常的开发过程中&#xff0c;经常会遇到一些串行或者并行的业务流程问题&#xff0c;而业务之间不必存在相关性。 在这样的场景下&#xff0c;使用策略和模板模式的结合可以很好的解决这个问题&#xff0c;但是使用编码的方式会使得文件太多,在业务的部分环节可以…

利用操作符解题的精彩瞬间

下面是链接为了解释练习2的并且还有与操作符相关的知识。 C语言与操作符相关的经典例题-CSDN博客 操作符详解&#xff08;上&#xff09;-CSDN博客 操作符详解&#xff08;下&#xff09;-CSDN博客 目录 练习1&#xff1a;在一个整型数组中&#xff0c;只有一个数字出现一…

burp靶场--xss下篇【16-30】

burp靶场–xss下篇【16-30】 https://portswigger.net/web-security/all-labs#cross-site-scripting 实验16&#xff1a;允许使用一些 SVG 标记的反射型 XSS ### 实验要求&#xff1a; 该实验室有一个简单的反射型 XSS漏洞。该网站阻止了常见标签&#xff0c;但错过了一些 S…

【Midjourney】关于标准模型的几个按钮都有什么用

当用户在Midjourney Bot所在的服务发送/settings命令时就能调出设置窗口&#xff0c;本文将介绍该窗口中的各个按钮都有什么作用。 1.RAW Mode 依照官方的描述来看V5.2模型似乎带有自动优化功能&#xff0c;会对用户输入的关键词空白描述进行补全和优化&#xff0c;以便修复所…

如何快速记忆小鹤双拼键位图?

记忆方法&#xff1a;韵母表 图形 最常用字 韵母表&#xff1a;双拼的基础 图形&#xff1a;帮助新手快速联想回忆 最常用字&#xff1a;快速打字基础 一、单韵母&#xff08;紫色方块&#xff09; 一一对应如下表&#xff1a; 单韵母aoeiu、AOEIV 二、复韵母—箭矢型&am…

【springboot网页时装购物系统】

前言 &#x1f31e;博主介绍&#xff1a;✌全网粉丝15W,CSDN特邀作者、211毕业、高级全栈开发程序员、大厂多年工作经验、码云/掘金/华为云/阿里云/InfoQ/StackOverflow/github等平台优质作者、专注于Java、小程序技术领域和毕业项目实战&#xff0c;以及程序定制化开发、全栈…

人工智能与机器学习——开启智能时代的里程碑

写在前面 前言人工智能与机器学习的概述监督学习、无监督学习和强化学习的基本原理监督学习&#xff1a;无监督学习&#xff1a;强化学习&#xff1a; 机器学习的算法和方法常见的机器学习算法和方法线性回归&#xff1a;决策树&#xff1a;支持向量机&#xff1a;神经网络&…

Ubuntu20.04添加桌面启动、侧边栏启动和终端启动

桌面启动 新建XX.desktop文件 在桌面新建一个XX.desktop文件&#xff0c;以QtCreator为例。 &#xff08;注意这里不能使用sudo&#xff0c;因为这样会把文件的权限归为root&#xff0c;导致后续设置可执行程序不方便&#xff09; gedit qtcreator.desktop在XX.desktop文件中…

Chiplet,汽车“芯”风向

异构集成、高速互联、算力灵活可扩展正在成为新一轮汽车芯片竞争的焦点。尤其是随着以ChatGPT为代表的大数据、大模型产品在车端的落地&#xff0c;对于芯片的要求还在持续提升。 本周&#xff0c;12家日本汽车制造商&#xff08;包括丰田、日产、本田等&#xff09;、零部件制…

[技术杂谈]nvidia-smi参数和显示信息解释

GPU&#xff1a;本机中的GPU编号&#xff0c;从0开始&#xff0c;上图为0&#xff0c;一块GPU Fan&#xff1a;风扇转速&#xff08;0%-100%&#xff09;&#xff0c;N/A表示没有风扇 Name&#xff1a;GPU名字/类型&#xff0c;上图为NVIDIA GeForce . . . Temp&#xff1a;GPU…

《Numpy 简易速速上手小册》第10章:Numpy案例研究和实践技巧(2024 最新版)

文章目录 10.1 实际案例分析10.1.1 基础知识10.1.2 完整案例&#xff1a;天气数据分析10.1.3 拓展案例 1&#xff1a;股票价格分析10.1.4 拓展案例 2&#xff1a;信号处理 10.2 Numpy 最佳实践10.2.1 基础知识10.2.2 完整案例&#xff1a;高效数组操作10.2.3 拓展案例 1&#x…

2023年博客总结反思与未来规划

前言&#xff1a; 24将至&#xff0c;23收尾&#xff0c;作为一名电信专业的大一学生&#xff0c;我在这后半年学习了不少的编程知识&#xff0c;也写了几十篇博客&#xff0c;今天想反思自己在博客创作和知识学习中的不足并且对未来进行规划。 种下一棵树最好的时间是10年前&…

Doris 与 Clickhouse 对比(一)

1. 常用引擎 ☕️ Doris 表数据模型 duplicate key &#x1f3ac; 场景&#xff1a;适用于数据无需提前聚合的分析业务。 ⚠️ 注意点&#xff1a;只指定排序列&#xff0c;相同的行并不会合并。 unique key &#x1f3ac; 场景&#xff1a;适用于有更新需求的业务。 ⚠…

心灵鸡汤美文:温暖你的每一寸心田

1.人生就像一杯茶&#xff0c;不会苦一辈子&#xff0c;但总会苦一阵子。只有经历过苦涩&#xff0c;才能品味到甜美的滋味。 2.每一次失败都是一次宝贵的经验&#xff0c;它教会我们如何更好地面对困难和挑战。不要害怕失败&#xff0c;因为失败是成功的前奏。 3.人生最重要的…