“别让自我被拯救~”
谈谈归并与分治
当我们首次接触排序算法时,一定对所谓 "归并"方式排序的算法感到头疼~ 因为,我们难以形象出其不断 "分离"时,各个区域的状态。然而,即便 "归并"排序算法的学习是艰难,但掩盖不住其相较于其他算法而言,拥有更优时间复杂度,而迫使我们不得不盯着头疼的代码苦思冥想。并且,"归并"思想不仅仅体现在排序中,更能成为我们解决实际问题时的一个方略方法。
归并核心之一就在于 "分治",那么啥子叫 "分治"? 简单来说,就是 "分而治之,大事化小" 。
假设我们要对举例数组元素进行升序排序arr[8] = { 6,1,2,7,3,4,8,0}。要排升序需不需要知道每个数与每个数之间的大小关系?需要! 所以我们得从 n=1 开始与 n-1个数进行大小关系的比较~ 唔,这一套下来,直接给时间复杂度 干上了 O(N^2),与冒泡的效率难分伯仲了。
或许我们可以缩小比较范围,与n个数比较换成与 n / 2 个数比较呢? 或者 n / 4个数? 甚至 n==1。
归并 vs 快排
快排与归并都是通过在既定区间,对数组内元素值进行排序,但两者有根本上的区别~
排序数组
没什么好解析题目的,咱们直接以归并的思想拿这道题练练手~
递归版本:
class Solution {
public:void _MergeSort(vector<int>& nums,int l,int r,vector<int>& tmp){// 当归并的区间不存在~if(l >= r) return;// 划分区域int mid = (l + r) >> 1;// 进行左右区间划分分治 --> 我们就认为MergeSort可以给我们完成划分分治工作_MergeSort(nums,l,mid,tmp);_MergeSort(nums,mid+1,r,tmp);// 走到这里说明 分治已经做完~ 此时开始归并了// 此时区域被划分为 {l,mid} {mid+1,l} // 我们需要保证归并数组的 有序性 所以我们得将这两个区间归并排序// 我们额外的数组起作用了~int begin1 = l,end1 = mid;int begin2 = mid+1,end2 = r;int index_tmp = l; // 注意tmp模拟的是nums 所以这里的index不能为0 根据你的区域划分l设置初始值while(begin1 <= end1 && begin2 <= end2){if(nums[begin1] <= nums[begin2]) tmp[index_tmp++] = nums[begin1++];else tmp[index_tmp++] = nums[begin2++];}while(begin1 <= end1) tmp[index_tmp++] = nums[begin1++];while(begin2 <= end2) tmp[index_tmp++] = nums[begin2++];//我们现在只是将排序好的数组放在了 tmp 现在我们需要将这些元素 放置回去// 防止多少呢? tmp归并了多少: index_tmp// 从哪里开始放呢 l// for(int i=l;i<index_tmp;++i) nums[i] = tmp[i];memcpy(&nums[0] + l,&tmp[0] + l,sizeof(int) * (r-l+1));}void MergeSort(vector<int>& nums,int l,int r){vector<int> tmp(nums.size());_MergeSort(nums,l,r,tmp);}vector<int> sortArray(vector<int>& nums) {MergeSort(nums,0,nums.size()-1);return nums;}
};
非递归版本:
有些时候考虑堆栈递归的深度,可能会要求使用非递归的方式实现归并排序~我们可以使用希尔排序的思想,依据gap的间距划分待排序区间~
算法思想: 不过与 shell排序明显不同的地方是,其gap间距尽可能从大值选取,逐渐减小在归并中,初始取gap值为1,gap值逐渐增大。 当然这就对应了 递归版的,将左右区间缩减到1个元素
细节问题:
class Solution {
public:void _MergeSort(vector<int>& nums, int start1, int end1,int start2, int end2, vector<int>& tmp){int index_tmp = start1;int begin = start1;while (start1 <= end1 && start2 <= end2){if (nums[start1] <= nums[start2]) tmp[index_tmp++] = nums[start1++];else tmp[index_tmp++] = nums[start2++];}while (start1 <= end1) tmp[index_tmp++] = nums[start1++];while (start2 <= end2) tmp[index_tmp++] = nums[start2++];for (int i = begin;i < index_tmp;++i) nums[i] = tmp[i];}void MergeSort(vector<int>& nums, int l, int r){vector<int> tmp(nums.size());int gap = 1;while (gap < nums.size()){for (int i = 0;i < nums.size();i += 2 * gap){// 为什么需要-1? 这里是为了满足 [1,1]归并int start1 = i, end1 = i + gap - 1;int start2 = i + gap, end2 = i + 2 * gap - 1;// 越界if (end1 > r || start2 > r) break;// 不足if (end2 > r) end2 = r;// 进行归并_MergeSort(nums, start1, end1, start2, end2, tmp);}gap *= 2;}}vector<int> sortArray(vector<int>& nums) {MergeSort(nums, 0, nums.size() - 1);return nums;}
};
排序链表
区别于数组排序,我们可以通过拿到数组的首元素地址从而访问所有的元素值,如果想要交换两个元素的位置直接使用库函数 std::swap()。但,现在我们需要排序的数链表,其节点内部包含着val,我们不能像数组那样对齐进行随机访问,所以,意向使用快排或者shell完成对题目规定内O(N*LOGN),是行不通的~\
细节问题:
所以,我们把目光聚焦在归并排序~ 区别于数组排序,我们可以通过俩左右下标计算出mid划分区域,在归并排序中,我们借助快慢指针完成对前后区域的划分~
class Solution {
public:ListNode* MergeSort(ListNode* head){if(head == nullptr || head->next == nullptr) return head;ListNode* fast = head->next,*slow = head;while(fast && fast->next){slow = slow->next;fast = fast->next->next;}// 这里进行切断ListNode* mid = slow->next;slow->next = nullptr;// 分治ListNode* start1 = MergeSort(head);ListNode* start2 = MergeSort(mid);// 归并ListNode* newhead = new ListNode(-1);ListNode* tail = newhead;while(start1 && start2){if(start1->val <= start2->val){tail->next = start1;start1 = start1->next;}else{tail->next = start2;start2 = start2->next;}tail = tail->next;}// 归并剩下的tail->next = start1 == nullptr ? start2 : start1;tail = newhead->next;delete newhead;return tail;}ListNode* sortList(ListNode* head) {return MergeSort(head);}
};
合并K个升序链表
一个很简单的思路,就是给数组内的链表节点继续排升序。我们可以利用 优先级队列,把vector中的所有元素插入进该数组,构建小堆。这样我们每次取堆顶数据时,都可以取到最小值,从而实现所谓的合并~
class Solution {
public:ListNode* mergeKLists(vector<ListNode*>& lists) {// 这里我们需要重载比较函数 因为默认函数greater不支持auto func = [](ListNode* node1,ListNode* node2)->bool{return node1->val > node2->val;};// 构建小堆priority_queue<ListNode*,vector<ListNode*>,decltype(func)> heap;for(auto l:lists) if(l) heap.push(l);ListNode* newhead = new ListNode(-1);ListNode* tail = newhead;while(!heap.empty()){ListNode* front = heap.top();heap.pop();tail->next = front;tail = tail->next;if(front->next) heap.push(front->next);}tail = newhead->next;delete newhead;return tail;}
};
考虑优先级队列中的个数是既定的k(数组里的首元素)个,一个节点的插入、删除花费的时间复杂度为 O(logK),总的效率控制在O(kn * logK)。
再次来会看题目:
嘶~ 这难道不就是叫我们进行区间合并嘛? 我们似乎在哪里做过~ 是的! 归并排序中,当我们进行区域分治后,就是区域排序! 然而,现在更为简单,区域是给你划分好了,就连区域内的元素也是给你按 升序排序的!我们目前要做的,无非就是 "合并"!
class Solution {
public:ListNode* mergeList(ListNode* left,ListNode* right){// left||right都可能出现nullptr 即是 不能组成归并的 直接返回 不为空的if(left == nullptr) return right;if(right == nullptr) return left;ListNode* newhead = new ListNode(-1);ListNode* tail = newhead;while(left && right){if(left->val <= right->val){tail->next = left;left = left->next;}else{tail->next = right;right = right->next;}tail = tail->next;}if(left) tail->next = left;if(right) tail->next = right;tail = newhead->next;delete newhead;return tail;}ListNode* _mergeKLists(vector<ListNode*>& lists,int l,int r){if(l > r) return nullptr;// 如果只存在一组 直接返回这个链表if(l == r) return lists[l];int mid = (l+r) >> 1;// 划分为极小的ListNode* left = _mergeKLists(lists,l,mid);ListNode* right = _mergeKLists(lists,mid+1,r);// 进行两两归并return mergeList(left,right);}ListNode* mergeKLists(vector<ListNode*>& lists) {return _mergeKLists(lists,0,lists.size()-1);}
};
交易逆序对的总数
所谓逆序对,就是指: "前后数存在大小关系,大的在前,小的在后"。我们要做的就是统计,符合条件的逆序对的个数~
正如图示,我们可以采用暴力解法,从i=0开始,套两层的for循环,可以暴搜完成逆序对总数的查找和统计。
int ret = 0;for(int i=0;i<n;++i)for(int j=i+1;j<n;++j){if(nums[i] > nums[j]) ret++;}
哈哈哈,不过多数时候(或许压根任何时候)刷题软件的后端使用到的令人发指的测试数据往往会让你的代码经不起半点抗击打而原形毕露—— "超过时间限制"。
或许,我们可以在暴搜的基础上做出一定的优化,暴搜花费的时间在哪里??"你这难道不是说屁话嘛??不就是那咔咔两行敲上去的双层for?"。对 ! 这是原因,但这只是表象而非根因~ 在暴搜的过程中,并没有保存之前元素艰难爬行于那千疮百孔般代码所留下的记录,它们的贡献被历史遗忘.... 当然,本题并不会按照这样的思路解题,但却是为探索新方法提供了别样的视角~
那么回归本题,咱们又该从哪些地方下手?
为什么选择归并?
🎃 这与我们归并的过程几乎一样~
我们引入一个mid指针,就会将整个数组分为两个部分。逆序对如何产生呢?它可能来自:左半边数组、也可能来自右半边数组、还可能一个来自左半数组,另一个来自右半数组...所以,一旦我们想要求整个数组的逆序对,就将这三种情况相加即可~
先求左半数组逆序对、再求右半数组、最后是左、右逆序对,这就同归并排序一样,先排序左边数组、再继续右边数组的排序,最后进行归并。我们可以在这个过程中,顺便将左、右两边的逆序对求出来,最后把这些结果相加即可~
为什么可行?
我们在归并之前就已经统计完成了逆序对,并且归并带来的有序性,能让我们快速统计出左、右部分数组的逆序对,而不需要像暴搜那样枚举所有的情况~
######### 按照升序排 #############
class Solution {
public:vector<int> tmp;int mergesort(vector<int>& nums,int left,int right){// 不存在的逆序对if(left >= right) return 0;int mid = (left + right) >> 1, ret = 0;// 加左、右两边ret += mergesort(nums,left,mid);ret += mergesort(nums,mid+1,right);// 合并数组的情况下:统计ret 左、右部分// 如果是升序 --> 找右半小值int cur1 = left,cur2 = mid + 1,i = left;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){//找大值// 这里赋值给tmp是满足归并tmp[i++] = nums[cur1++];}else{// 进行更新 统计右半区ret += mid - cur1 + 1;tmp[i++] = nums[cur2++]; // cur2小进入先进入合并}}// 处理剩余数组while(cur1 <= mid) tmp[i++] = nums[cur1++];while(cur2 <= right) tmp[i++] = nums[cur2++];// 归并for(int i=left;i<=right;++i) nums[i] = tmp[i];return ret;}int reversePairs(vector<int>& record) {tmp.resize(record.size());return mergesort(record,0,record.size()-1);}
};######### 按照降序排 #############
class Solution {
public:vector<int> tmp;int mergesort(vector<int>& nums,int left,int right){// 不存在的逆序对if(left >= right) return 0;int mid = (left + right) >> 1, ret = 0;// 加左、右两边ret += mergesort(nums,left,mid);ret += mergesort(nums,mid+1,right);// 合并数组的情况下:统计ret 左、右部分// 如果是降序 ---> 找左边值int cur1 = left,cur2 = mid + 1,i = left;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){// 不行cur2太大了 完完全全统计不了rettmp[i++] = nums[cur2++];}else{// 统计右半区ret += right - cur2 + 1;tmp[i++] = nums[cur1++];}}// 处理剩余数组while(cur1 <= mid) tmp[i++] = nums[cur1++];while(cur2 <= right) tmp[i++] = nums[cur2++];// 归并for(int i=left;i<=right;++i) nums[i] = tmp[i];return ret;}int reversePairs(vector<int>& record) {tmp.resize(record.size());return mergesort(record,0,record.size()-1);}
};
本篇到此结束,感谢你的阅读
祝你好运,向阳而生~