AK F.*ing leetcode 流浪计划之归并排序

欢迎关注更多精彩
关注我,学习常用算法与数据结构,一题多解,降维打击。

一、简介

归并排序是基础排序之一,其核心算法是合并2个有序数组。利用合并有序数组算法,自底向上先合并2个数,再4个数,直到整个数组有序。在面试中也经常被问到其原理,甚至会让手写归并排序。合并有序数组算法在解题中经常用到,学习与解理归并排序算法原理,对于解决算法题都是非常有帮助的。防止在面试时眼高手低。

这里主要以int为例,讲解原理,以及排序的实现。最后结合题目讲解归并排序的应用。

本文中归并原理参照严版《数据结构与算法》,公众号内回复“数据结构”可以获取pdf电子版。

二、基本概念

归并排序定义为MergeSort(arr, low, high)

每次先用再对左右部分分别进行排序 MergeSort(arr, low, mid), MergeSort(arr, mid+1, high)保证左右子数组有序再调用merge进行左右子数组合并。

其核心算法是merge算法merge(arr, low, high)。

其作用是将arr[low] 到 arr[high]合并,保持有序。

三、作用

  1. 对数组进行排序。
  2. 对链表进行排序。
  3. 合并2个有序数组/链表。
  4. 合并多个有序数组。
  5. 求解数组逆序对。

四、方法定义及算法

方法定义

    // 主要操作:
    Merge(arr, low, high) // 核心合并
	MergeSort(arr, low, high) // 归并排序

算法描述

merge过程
图1 merge算法过程

这里merge过程,先把原数组复制到另一个数组,然后最后结果从小到大依次填入到原数组中。

Merge(arr, low, high): // merge算法, 如图动画
	copyArr <- arr[low:high+1]
	mid <- (low+high)/2
	i<- low
	j<- mid+1
	s<-low
	while (i<=mid || j<=high) {
		// j>high 时,只能取copyArr[i-low]
		// 否则,就尝试比较copyArr[i-low]<=copyArr[j-low]
		if (j>high || (i<=mid && copyArr[i-low]<=copyArr[j-low])) {
			arr[s]<-copyArr[i-low]
			s<-s+1
			i<-i+1
		} else { // 除以上情况就只能取copyArr[j-low]
			arr[s]<-copyArr[j-low]
			s<-s+1
			j<-j+1
		}
	}


MergeSort(arr, low, high): // 归并排序
	if low>=high { // 少于一个元素,不用排序
		return
	}
	
	mid <- (low+high)/2
	MergeSort(arr, low, mid) // 先使左边有序
	MergeSort(arr, mid+1, high) // 再使右边有序
	Merge(arr, low, high) // 合并左右两边,使整个有序


五、具体实现

package main

import (
	"fmt"
)

func Merge(arr []int, low, high int) {
	copyArr := make([]int, high-low+1)
	copy(copyArr, arr[low:high+1])
	mid := (low + high) / 2
	for i, j, s := low, mid+1, low; i <= mid || j <= high; s++ {
		// j>high 时,只能取arr[i-low]
		// 否则,就尝试比较arr[i-low]<=arr[j-low]
		if j > high || (i <= mid && copyArr[i-low] <= copyArr[j-low]) {
			arr[s], i = copyArr[i-low], i+1
		} else { // 除以上情况就只能取arr[j-low]
			arr[s], j = copyArr[j-low], j+1
		}
	}
}

func MergeSort(arr []int, low, high int) {
	if low >= high { // 少于一个元素,不用排序
		return
	}

	mid := (low + high) / 2
	MergeSort(arr, low, mid)    // 先使左边有序
	MergeSort(arr, mid+1, high) // 再使右边有序
	Merge(arr, low, high)       // 合并左右两边,使整个有序
}

func main() {
	arr := []int{10, 6, 7, 12, 18, 12, 1, 2, 4, 3}
	MergeSort(arr, 0, len(arr)-1)
	fmt.Println(arr)

	arr = []int{1, 1, 1, 1, 1}
	MergeSort(arr, 0, len(arr)-1)
	fmt.Println(arr)

	arr = []int{}
	MergeSort(arr, 0, len(arr)-1)
	fmt.Println(arr)

	arr = []int{1}
	MergeSort(arr, 0, len(arr)-1)
	fmt.Println(arr)
}


复杂度分析,MergeSort最多递归log(n) 层, 每层合并总数是N个数,整体复杂度O(nlog(n))。

六、牛刀小试

练习1 验证代码是否正确,排序应用

题目链接 力扣

题目大意

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入:nums = [5,2,3,1]
输出:[1,2,3,5]
示例 2:

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

提示:

1 <= nums.length <= 50000
-50000 <= nums[i] <= 50000

题目解析

对原数组进行排序,调用归并排序方法即可。

AC代码

func Merge(arr []int, low, high int) {
	copyArr := make([]int, high-low+1)
	copy(copyArr, arr[low:high+1])
	mid := (low + high) / 2
	for i, j, s := low, mid+1, low; i <= mid || j <= high; s++ {
		// j>high 时,只能取arr[i-low]
		// 否则,就尝试比较arr[i-low]<=arr[j-low]
		if j > high || (i <= mid && copyArr[i-low] <= copyArr[j-low]) {
			arr[s], i = copyArr[i-low], i+1
		} else { // 除以上情况就只能取arr[j-low]
			arr[s], j = copyArr[j-low], j+1
		}
	}
}

func MergeSort(arr []int, low, high int) {
	if low >= high { // 少于一个元素,不用排序
		return
	}

	mid := (low + high) / 2
	MergeSort(arr, low, mid)    // 先使左边有序
	MergeSort(arr, mid+1, high) // 再使右边有序
	Merge(arr, low, high)       // 合并左右两边,使整个有序
}

func sortArray(nums []int) []int {
    MergeSort(nums, 0, len(nums)-1)
    return nums
}

练习2 利用归并排序原理,对链表进行排序

题目链接:力扣,

题目大意

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

题目解析

可以有2种方法:归并排序,爬楼梯排序(C++ 列表排序算法)

参考之前的文章:头条面试题之链表排序

练习3 合并2个有序数组

题目链接:力扣

题目大意

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。

初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。

示例 1:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
示例 2:

输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]

提示:

nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-10^9 <= nums1[i], nums2[i] <= 10^9

题目解析

由于nums1空间是m+n, 可以利用倒序遍历进行merge, 实现O(1)空间复杂度。

AC代码

func merge(nums1 []int, m int, nums2 []int, n int)  {
    for i, j, s := m-1, n-1, m+n-1; s>=0;s-- {
        if j<0 || (i>=0 && nums1[i]>=nums2[j]) {
            nums1[s]=nums1[i]
            i--
        } else {
            nums1[s]=nums2[j]
            j--
        }
    }
}

练习4 合并多个有序链表

题目链接:力扣

题目大意

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例 2:

输入:lists = []
输出:[]
示例 3:

输入:lists = [[]]
输出:[]

提示:

k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i] 按 升序 排列
lists[i].length 的总和不超过 10^4

题目解析

有2种方法:1. 利用堆每次取到最小的数字放到ans中。2. 利用归并排序思想,对链表两两合并,直到剩下一个链表。

由于这里讲解的是归并排序的应用,给出的解答没有涉及到堆。

思路及优化

暴力思路如下:

func mergeKLists(lists []*ListNode) *ListNode {
  ans := &ListNode{}
  for _, l:= range lists{ // 每次拿一个链表合并到最终结果中
    ans = Merge(ans, l)
  }
  
  return ans
}

复杂度分析,最外层一共是len(lists) 次,每次最差情况是 所有链表长度总和(L), 总复杂度是 O(len(lists)*L), 大约是 500 *(10^4)*500 = 25*10^8 操作数太大了,会超时。

优化如下:

/*
利用归并思想,先将左右2边分别合并成一个链表,最后再合并左右2边成一个链表。
*/
func mergeKLists(lists []*ListNode) *ListNode {
  if len(lists) == 0 {
		return nil
	}
	if len(lists) == 1 {
		return lists[0]
	}
	if len(lists) == 2 {
		return Merge(lists[0], lists[1])
	}
  // 以上情况如果数组<=2元素就直接合并了
  // 否则就先分成左右2边合并,再整体合并
	mid := len(lists) / 2
	a := mergeKLists(lists[:mid])
	b := mergeKLists(lists[mid:])
	return Merge(a, b)
}

复杂度分析:根据归并原理,一共需要log(len(lists))层,每层总操作数是所有数字个数L, 总体复杂度是 O(log(len(lists))*L), 大约是 log(500) *(10^4)*500 = 45*10^6 操作数,勉强可以接受。

AC代码

func Merge(a, b *ListNode) *ListNode {
	ans := &ListNode{}
	temp := ans

	for ;a != nil || b != nil;temp = temp.Next {
		if b == nil || (a != nil && a.Val <= b.Val) {
			temp.Next = a
			a = a.Next
		} else {
			temp.Next = b
			b = b.Next
		}
	}
    temp.Next = nil
	return ans.Next
}

func mergeKLists(lists []*ListNode) *ListNode {
	if len(lists) == 0 {
		return nil
	}
	if len(lists) == 1 {
		return lists[0]
	}
	if len(lists) == 2 {
		return Merge(lists[0], lists[1])
	}
	mid := len(lists) / 2
	a := mergeKLists(lists[:mid])
	b := mergeKLists(lists[mid:])
	return Merge(a, b)
}

七、总结

主要内容:

  1. 归并排序是基础排序之一,其核心算法是合并2个有序数组。利用合并有序数组算法,自底向上先合并2个数,再4个数,直到整个数组有序。
  2. 介绍了merge算法原理。
  3. 作用:1)对数组进行排序, 2)对链表进行排序, 3)合并2个有序数组/链表, 4) 合并多个有序数组, 5)求解数组逆序对。

笔者水平有限,有写得不对或者解释不清楚的地方还望大家指出,我会尽自己最大努力去完善。

下面我精心准备了几个流行网站上的题目(首先AK F.*ing leetcode),给大家准备了一些题目,供大家练习参考。干他F.*ing (Fighting?)。

八、实战训练

代码基础训练题

光说不练假把式,学完了怎么也要实操一下吧,快快动用把刚才那4题A了。

  1. 排序应用 力扣
  2. 利用归并排序原理,对链表进行排序:力扣
  3. 合并2个有序数组:力扣
  4. 合并多个有序链表: 力扣

AK leetcode

leetcode相关题目都在下面了,拿起武器挨个点名呗。

  1. 力扣
  2. 力扣
  3. 力扣
  4. 力扣
  5. 力扣
  6. 力扣
  7. 力扣
  8. 力扣
  9. 力扣
  10. 力扣
  11. 力扣
  12. 力扣
  13. https://leetcode.com/problems/sort-list/

本人码农,希望通过自己的分享,让大家更容易学懂计算机知识。

在这里插入图片描述