王清欢Randy 王清欢Randy
首页
  • 编程语言

    • C/C++ 学习笔记
    • Golang 学习笔记
  • 算法分析

    • LeetCode 刷题笔记
  • 操作系统

    • Linux 基础
    • Vim 实用技巧
    • Shell 脚本编程
    • GDB 学习笔记
  • 开发工具

    • Git 学习笔记
  • 分布式理论

    • 共识算法
    • 分布式事务
  • 数据库内核

    • PostgreSQL
    • Postgres-XL
  • hidb
  • pgproxy
  • 实用技巧
  • 学习方法
  • 资源分享
GitHub (opens new window)
首页
  • 编程语言

    • C/C++ 学习笔记
    • Golang 学习笔记
  • 算法分析

    • LeetCode 刷题笔记
  • 操作系统

    • Linux 基础
    • Vim 实用技巧
    • Shell 脚本编程
    • GDB 学习笔记
  • 开发工具

    • Git 学习笔记
  • 分布式理论

    • 共识算法
    • 分布式事务
  • 数据库内核

    • PostgreSQL
    • Postgres-XL
  • hidb
  • pgproxy
  • 实用技巧
  • 学习方法
  • 资源分享
GitHub (opens new window)
  • 基础算法

    • 双指针

      • 双指针基础
      • 碰撞指针
      • 快慢指针
      • 滑动窗口
    • 二分查找

      • 基础应用
      • 边界收缩
      • 局部有序
    • 排序算法

      • 01八大排序
      • 02快速排序
      • 03归并排序
        • 03 归并排序
          • 归并排序简介
          • 493 翻转对
          • 148 排序链表
      • 04桶排序
      • 05堆排序
    • 优先搜索

      • 01递归
      • 02网格结构深度优先搜索
      • 03树结构深度优先搜索
      • 04图结构深度优先搜索
      • 05网格结构广度优先搜索
      • 06树结构广度优先搜索
      • 07图结构广度优先搜索
    • 回溯算法

      • 01递归
      • 02 子集问题
      • 03 全排列问题
      • 04 组合问题
      • 05 回溯搜索问题
  • 基础数据结构

    • 线性表与哈希表

      • 01数组
      • 02栈和队列
      • 03单调栈
      • 04优先队列
      • 05双端队列
      • 06哈希表
      • 07多重集合
      • 08前缀和
      • 09数据结构设计
    • 字符串

      • 01字符串比较
      • 02回文字符串
      • 03字符串匹配
      • 04字符串算术表达式
    • 单链表

      • 01链表基础操作
      • 02链表遍历
    • 二叉树

      • 01二叉树的属性
      • 02二叉树的操作
      • 03层次遍历
      • 04前中后序遍历
      • 05二叉搜索树的属性
      • 06二叉搜索树的操作
      • 07字典树
      • 08二叉搜索树BST
    • 图

      • 01二分图
      • 02拓扑排序
      • 03并查集
      • 04最小生成树
      • 05最短路径
  • 进阶算法

    • 贪心算法

      • 01跳跃游戏
      • 02分配问题
      • 03区间问题
    • 分治策略

    • 动态规划

      • 01一维动态规划
      • 02二维动态规划
      • 03分割型动态规划
      • 04子序列问题
      • 05背包问题
      • 06字符串编辑问题
      • 07股票交易问题
  • 其他内容

    • 数学问题

      • 01公倍数与公因数
      • 02质数问题
      • 03进制转换问题
      • 04数字字符串求和问题
      • 05众数问题
      • 06中位数问题
      • 07数字处理问题
      • 08随机数问题
    • 位运算

      • 01位运算基础
      • 02妙用异或运算
      • 03二进制特性
  • LeetCode刷题笔记
  • 基础算法
  • 排序算法
王清欢
2023-03-24
目录

03归并排序

# 03 归并排序

# 归并排序简介

算法思想:

​ 归并排序的核心思想是采用分治策略,将整个数组的排序任务分类为两个子问题,前一半排序和后一半排序,然后整合两个有序部分完成整体排序。即把数组分为若干个子序列,直到单个元素组成一个序列,然后将各阶段得到的序列组合在一起得到最终完整排序序列。

​ 归并排序任务可以如下分治完成:

1. 把前一半排序
2. 把后一半排序
3. 把两半归并到一个新的有序数组,然后再拷贝回原数组,排序完成。

执行样例:

输入:[29,10,14,37,14,25,10]

归并排序

算法实现:

// 将数组 a 的局部 a[s,m] 和 a[m+1,e] 合并到 tmp, 并保证 tmp 有序,然后再拷贝回 a[s,m]
void merge(vector<int>& arr, int start, int mid, int end, vector<int> tmp){
    int pTmp = 0;
    int pLeft = start; int pRight = mid+1;
    while(pLeft<=mid&&pRight<=end){
        if(arr[pLeft] < arr[pRight]){
            tmp[pTmp++] = arr[pLeft++];
        }else{
            tmp[pTmp++] = arr[pRight++];
        }
    }
    while(pLeft<=mid){
        tmp[pTmp++] = arr[pLeft++];
    }
    while (pRight<=end)
    {
        tmp[pTmp++] = arr[pRight++];
    }
    for(int i=0;i<pTmp;i++){
        arr[start+i] = tmp[i];
    }
}

// 归并排序递归调用,先排前半部分,在排后半部分,最后将两部分结果合并
void mergeSort(vector<int>& arr, int start, int end, vector<int> tmp){
    if(start < end){
        int mid = start + (end-start)/2;
        mergeSort(arr,start,mid,tmp);
        mergeSort(arr,mid+1,end,tmp);
        merge(arr,start,mid,end,tmp);
    }
}

# 493 翻转对 (opens new window)

给定一个数组 nums ,如果 i < j 且 nums[i] > 2*nums[j] 就将 (i,j) 称作一个翻转对,返回给定数组中翻转对的数量。

输入一个数组,输出一个整数表示数组中翻转对的个数

输入: [2,4,3,5,1]
输出: 3
解释: (4,1),(3,1),(5,1)三个翻转数对

解析:

​ 本题的实质是求数组中的逆序数,但是本题中对逆序数有新的定义。

​ 我们基于归并排序算法采用分治策略:将排列分为两部分,先求左半部分的翻转对,然后求右半部分的翻转对;最后算两部分之间存在的翻转对,时间复杂度 O(nlogn)。

​ 左半边和右半边都是排好序的。例如,都是从大到小排序的,左右半边只需要从头到尾各扫一遍,就可以找出由两边各取一个数构成的翻转对数。

​ 下面给出了一种较为容易理解的实现方法,在归并排序代码的基础上做了很小的修改,就是当左侧元素大于右侧时开始寻找翻转对。但是,本题的数据规模最大可达到50000,如果使用这种简单循环遍历将导致超时。

class Solution {
public:
    void mergeAndCount(vector<int>& nums, int start, int mid, int end, vector<int> tmp, int& count){
        if(start>end) return;
        int pTmp = 0;
        int pLeft = start, pRight = mid+1;
        while(pLeft<=mid && pRight<=end){
            if(nums[pLeft] < nums[pRight]){
                tmp[pTmp++] = nums[pLeft++];
            }else{
                // 这种循环遍历方法将导致超时
                for(int i=pLeft;i<=mid;++i){
                    if(nums[i] > (long long)2*nums[pRight]){
                        ++count;
                    }
                }
                tmp[pTmp++] = nums[pRight++];
            }
        }
        while(pLeft<=mid){
            tmp[pTmp++] = nums[pLeft++];
        }
        while(pRight<=end){
            tmp[pTmp++] = nums[pRight++];
        }
        for(int i=0;i<pTmp;++i){
            nums[start+i] = tmp[i];
        }
    }

    void mergeSort(vector<int>& nums, int start, int end, vector<int> tmp, int& count){
        if(start<end){
            int mid = start + (end-start)/2;
            mergeSort(nums,start,mid,tmp,count);
            mergeSort(nums,mid+1,end,tmp,count);
            mergeAndCount(nums,start,mid,end,tmp,count);
        }
    }

    int reversePairs(vector<int>& nums) {
        int ans = 0;
        vector<int> tmp(nums.size());
        mergeSort(nums,0,nums.size()-1,tmp,ans);
        return ans;
    }
};

​ 为了降低时间复杂度,我们采用如下策略:如果左半边 A1 和右半边 A2 都是排好序的,我们就可以在线性时间内解决这个问题了。当然,也可以用二分查找来解决,但是时间复杂度就是线性对数的了。

  • 初始化两个指针i,j分别指向A1,A2的头部

  • 如果 A1[i] > 2*A2[j] ,那么A1[i]及其后面的所有元素都符合要求,更新答案并后移j,否则,后移i

  • 最后,合并A1, A2 以备解决后面更大的子问题使用,并返回前结果

class Solution {
public:
    int find_reversed_pairs(vector<int>& nums, int start, int end){
        int res = 0,mid = start + (end-start)/2;
        int i = start,j = mid+1;
        for(;i <= mid;i++){
            while(j <= end && (long)nums[i] > 2*(long)nums[j]) {
                res += mid - i + 1;
                j++;
            }
        }
        return res;
    }
    
    int merge_sort(vector<int>& nums, int start, int end, vector<int> tmp){
        if(start >= end) return 0;
        int mid = start + (end-start) / 2;
        
        int res = merge_sort(nums,tmp,start,mid) + 
                  merge_sort(nums,tmp,mid+1,end) + 
                  find_reversed_pairs(nums,start,end);
        
        int i = start,j = mid+1,ind = start;
        
        while(i <= mid && j <= end){
            if(nums[i] <= nums[j]) tmp[ind++] = nums[i++];
            else tmp[ind++] = nums[j++];
        }
        while(i <= mid) tmp[ind++] = nums[i++];
        while(j <= end) tmp[ind++] = nums[j++];
        
        for(int ind = start;ind <= end;ind++) nums[ind] = tmp[ind];
    
        return res;
    }
    
    int reversePairs(vector<int>& nums) {
        if(nums.empty()) return 0;
        vector<int>& tmp(nums.size());
        return merge_sort(nums,0,nums.size()-1,tmp);
    }
};

# 148 排序链表 (opens new window)

给定链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 ,要求在 O(nlogn)的时间复杂度内完成。

输入一个链表,输出一个按升序排列的链表。

输入:head = [4,2,1,3]
输出:[1,2,3,4]

解析:

​ 在完成本题之前可以先尝试 21 合并两个有序链表 (opens new window)、23 合并K个升序链表 (opens new window) 两题,也许对你解决本题有帮助。

​ 本题我们可以使用归并排序的思想完成。采用分治策略,先将链表划分为两个部分,然后在每个部分中进行排序。所以,我们需要完成如下两个任务:

  • 寻找链表中点划分链表
  • 排序局部链表,并合并结果

​ 第一个任务我们可以使用快慢指针完成,快指针一次走两步,慢指针一次走一步,这样当快指针到达链表末尾时,慢指针刚好指向链表中点。然后,我们递归地采用上诉方法将链表不断划分为更小的子问题,直到不可再分。这里需要注意一个常犯的错误:在寻找中点时不能直接以 nullptr 判断快指针是否达到终点,而是要将其与链表尾节点 tail 进行比较。

​ 第二个任务升序合并链表:分割完成之后,迭代合并两部分链表,将链表按照升序合并,最终形成完整的升序链表。

class Solution {
public:
	// 合并两个链表
    ListNode* merge(ListNode *l1, ListNode *l2){
        if(!l1 || !l2) return l1?l1:l2;
        ListNode dummy;
        ListNode *tail = &dummy;
        ListNode *p1 = l1, *p2 = l2;
        while(p1 && p2){
            if(p1->val < p2->val){
                tail->next = p1;
                p1 = p1->next; 
            }else{
                tail->next = p2;
                p2 = p2->next;
            }
            tail = tail->next;
        }
        tail->next = p1?p1:p2;
        tail = nullptr;
        return dummy.next;
    }
	
    // 寻找链表终点并递归划分链表
    ListNode* mergeSort(ListNode* head, ListNode* tail){
        if(!head) return nullptr;
        if(head->next == tail){
            head->next = nullptr;
            return head;
        }

        ListNode *slow = head, *fast = head;
        // // 错误示范,会导致越界
        // while(fast&&fast->next){
        //     fast = fast->next->next;
        //     slow = slow->next;
        // }
        while(fast != tail){
            slow = slow->next;
            fast = fast->next;
            if(fast != tail){
                fast = fast->next;
            }
        }
        ListNode* mid = slow;
        return merge(mergeSort(head,mid),mergeSort(mid,tail));
    }

    ListNode* sortList(ListNode* head) {
        return mergeSort(head,nullptr);
    }
};
上次更新: 2023/11/19, 12:55:48
02快速排序
04桶排序

← 02快速排序 04桶排序→

Theme by Vdoing | Copyright © 2023-2024 Wang Qinghuan | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式