【图解算法数据结构】(五)排序


目录

一、剑指 Offer 40. 最小的 k 个数

1.1 题求

1.2 求解

1.3 解答

二、剑指 Offer 41. 数据流中的中位数 ☆

2.1 题求

2.2 求解

2.3 解答

三、剑指 Offer 45. 把数组排成最小的数 ☆

3.1 题求

3.2 求解

3.3 解答

四、剑指 Offer 61. 扑克牌中的顺子

4.1 题求

4.2 求解

4.3 解答


一、剑指 Offer 40. 最小的 k 个数

1.1 题求

1.2 求解

法一:堆排序

# 52ms - 91.70% - 使用了 heapq 模块 API - 不推荐
class Solution:
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        import heapq
        return heapq.nsmallest(k, arr)
# 40ms - 99.45%
class Solution:
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        if not k:
            return []

        hp = arr[:k]  # 最大堆大小/容量
        hp = [-x for x in hp]  # 最小 -> 最大
        heapq.heapify(hp)  # list -> heap

        for i in range(k, len(arr)):
            if -arr[i] > hp[0]:  # 更大 (原本更小)
                heapq.heappop(hp)  # 弹出
                heapq.heappush(hp, -arr[i])  # 压入

        return [-x for x in hp]  # 最大 -> 最小

法二:插入排序

# 超出时间限制
class Solution:
    ''' https://blog.csdn.net/qq_39478403/article/details/107838299 '''
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        for i in range(1, len(arr)):
            cur_val = arr[i]  # 当前待比较并确定位置的值
            cur_idx = i  # 比较起点
            while cur_idx > 0 and cur_val < arr[cur_idx-1]:
                arr[cur_idx] = arr[cur_idx-1]  # 平移
                cur_idx -= 1  # 回滚
            arr[cur_idx] = cur_val  # 固定
            
        return arr[:k]

法三:选择排序 

# 超出时间限制
class Solution:
    ''' https://blog.csdn.net/qq_39478403/article/details/106980383 '''
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        n = len(arr)
        for i in range(n-1):
            min_idx = i  # 当前待确定的最小值位置索引
            for j in range(i+1, n):
                if arr[j] < arr[min_idx]:  # 找到未排序序列的最小值索引
                    min_idx = j
            arr[i], arr[min_idx] = arr[min_idx], arr[i]
 
        return arr[:k]

法四:归并排序

# 364ms - 7.06%
class Solution:
    ''' https://blog.csdn.net/qq_39478403/article/details/107020558 '''
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
    
        def merge_sort(array, left_index, right_index):
            ''' 归并排序函数 - 递归实现 '''
            if left_index >= right_index:               # 基本情况, 此时子数组长度为 1
                return
            
            mid = (left_index + right_index) // 2       # 中间元素索引
            merge_sort(array, left_index, mid)          # 左半区间子数组
            merge_sort(array, mid+1, right_index)       # 右半区间子数组
            merge(array, left_index, right_index, mid)  # 左、右子数组归并
            
        def merge(array, left_index, right_index, mid):
            ''' 归并函数 - 对两个子数组排序+组合 '''

            left_copy = array[left_index: mid+1]      # 左子数组 拷贝
            right_copy = array[mid+1: right_index+1]  # 右子数组 拷贝

            left_copy_index = 0        # 左拷贝子数组 元素索引
            right_copy_index = 0       # 右拷贝子数组 元素索引
            sorted_index = left_index  # 待修改 元素索引

            # 两个数组同时参与排序
            while (left_copy_index < len(left_copy)) and (right_copy_index < len(right_copy)):
                # 左小右大
                if left_copy[left_copy_index] <= right_copy[right_copy_index]:
                    array[sorted_index] = left_copy[left_copy_index]    # in-place
                    left_copy_index += 1  # 左进
                # 左大右小
                else:
                    array[sorted_index] = right_copy[right_copy_index]  # in-place
                    right_copy_index += 1  # 右进
                # 待修改位置索引+1
                sorted_index += 1

            # 右子数组提前耗尽, 左子数组剩余依次加入
            while left_copy_index < len(left_copy):    
                array[sorted_index] = left_copy[left_copy_index]
                left_copy_index += 1
                sorted_index += 1
                
            # 左子数组提前耗尽, 右子数组剩余依次加入
            while right_copy_index < len(right_copy): 
                array[sorted_index] = right_copy[right_copy_index]
                right_copy_index += 1
                sorted_index += 1
                
        merge_sort(arr, 0, len(arr))
        return arr[:k]

官方说明

# 252ms - 22.43%
class Solution:
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        
        def quick_sort(arr, l, r):
            # 子数组长度为 1 时终止递归
            if l >= r: 
                return
            
            # 哨兵划分操作(以 arr[l] 作为基准数)以数组某个元素(一般选取首元素)为 基准数 
            # 将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。
            i, j = l, r
            while i < j:
                while i < j and arr[j] >= arr[l]:  # 从右向左查找首个小于基准数的索引 j
                    j -= 1
                while i < j and arr[i] <= arr[l]:  # 从左向右查找首个大于基准数的索引 i
                    i += 1
                arr[i], arr[j] = arr[j], arr[i]  # 交换, 使小于基准数的在左边, 大于基准数的在右边
                
            # i, j 两哨兵相遇时跳出, 此时令 i=j 处于 l 交换
            arr[l], arr[i] = arr[i], arr[l]
            
            # 递归左、右子数组执行哨兵划分
            quick_sort(arr, l, i-1)
            quick_sort(arr, i+1, r)
        
        quick_sort(arr, 0, len(arr)-1)
        return arr[:k]

# 128ms - 37.08%
class Solution:
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        if k >= len(arr): 
            return arr
        
        def quick_sort(l, r):
            # 快速排序
            i, j = l, r
            while i < j:
                while i < j and arr[j] >= arr[l]: 
                    j -= 1
                while i < j and arr[i] <= arr[l]: 
                    i += 1
                arr[i], arr[j] = arr[j], arr[i]
            arr[l], arr[i] = arr[i], arr[l]
            
            # 根据题求优化
            if k < i: 
                return quick_sort(l, i-1)  # 代表第 k+1 小的数字在左子数组中,则递归左子数组
            if k > i: 
                return quick_sort(i+1, r)  # 代表第 k+1 小的数字在右子数组中,则递归右子数组
            # 未排序的部分顺序不重要, 在前 k 个即可
            return arr[:k]  # k == i 代表此时 arr[k] 即为第 k+1 小的数字,则直接返回数组前 k 个数
            
        return quick_sort(0, len(arr)-1)
# 优化 - 108ms - 43.08%
class Solution:
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        if not k:
            return []
        
        def quick_sort(arr, l, r):
            if l >= r:
                return
            
            i, j = l, r
            while i < j:
                while i < j and arr[l] <= arr[j]:
                    j -= 1
                while i < j and arr[l] >= arr[i]:
                    i += 1
                arr[i], arr[j] = arr[j], arr[i]
            arr[i], arr[l] = arr[l], arr[i]  # 首个元素/参考元素 与 中间元素交换, 此时 i=j
            
            if i > k:
                quick_sort(arr, l, i-1)  # 仅需排序左半部分, 因为右半部分不会取到
            elif i < k:
                quick_sort(arr, i+1, r)  # 仅需排序右半部分, 因为左半部分全都取到
            
        quick_sort(arr, 0, len(arr)-1)  # 快速排序
        return arr[:k]

1.3 解答

参考资料:

《剑指 Offer 第二版》

https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/ohvl0d/

https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/ohwddh/

https://www.jianshu.com/p/801318c77ab5

https://www.cnblogs.com/wangbin2188/p/13094033.html


二、剑指 Offer 41. 数据流中的中位数 ☆

2.1 题求

2.2 求解

法一:单链表

# 超出时间限制
class Node:
    def __init__(self, _val=None, _next=None):
        """
        node of single-linked list
        """
        self.val = _val
        self.next = _next


class MedianFinder:
    def __init__(self):
        """
        initialize your data structure here.
        """
        self.dummy = Node()  # 哨兵节点
        self.num = 0  # 节点计数

    def addNum(self, num: int) -> None:
        # 新增节点
        new_node = Node(num)  
        self.num += 1
        # 哨兵节点
        cur_node = self.dummy  
        # 寻找新节点插入位置
        while cur_node.next and cur_node.next.val < num:
            cur_node = cur_node.next
        # 插入新节点
        new_node.next = cur_node.next
        cur_node.next = new_node 

        return

    def findMedian(self) -> float:
        # 哨兵节点
        cur_node = self.dummy
        # 商及余数
        div, mod = divmod(self.num+1, 2)  
        # 商决定必走步数
        for _ in range(div):  
            cur_node = cur_node.next
        res = cur_node.val
        # 余数决定是否再走一步 (偶数个节点)
        if mod == 1:  
            res = (res + cur_node.next.val) / 2
          
        return res
            
                
# Your MedianFinder object will be instantiated and called as such:
# obj = MedianFinder()
# obj.addNum(num)
# param_2 = obj.findMedian()

官方说明

# 176ms - 89.22% - 最佳方案
from heapq import *

class MedianFinder:
    def __init__(self):
        self.A = []  # 小顶堆,保存较大的一半 +   (如 6, 5, 4)
        self.B = []  # 大顶堆,保存较小的一半     (如 -1, -2, -3)

    def addNum(self, num: int) -> None:
        # 若 小顶堆A 和 大顶堆B 个数不等, 则新值 num 加入 A, 再从 A 顶弹出取反加入 B, 从而 A B 个数相等
        if len(self.A) != len(self.B):
            heappush(self.A, num)
            heappush(self.B, -heappop(self.A))
        # 若 小顶堆A 和 大顶堆B 个数相等, 则新值 num 取反加入 B, 再从 B 顶弹出取反加入 A, 从而 A 多1个
        else:
            heappush(self.B, -num)  # 注意符号!!
            heappush(self.A, -heappop(self.B))

    def findMedian(self) -> float:
        # 若 小顶堆A 和 大顶堆B 个数不等, 仅取 A 顶
        # 若 小顶堆A 和 大顶堆B 个数相等, 则取 A 顶 和 B 顶反 的均值
        return self.A[0] if len(self.A) != len(self.B) else (self.A[0] - self.B[0]) / 2.0

2.3 解答

参考资料:

《剑指 Offer 第二版》

https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/5vd1j2/

https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/5v0zcc/


三、剑指 Offer 45. 把数组排成最小的数 ☆

3.1 题求

3.2 求解

官方说明

# 40ms - 83.11%
class Solution:
    def minNumber(self, nums: List[int]) -> str:
        def quick_sort(l , r):
            # 索引越界, 完成子集排序
            if l >= r:   
                return
            # 左、右指针起点
            i, j = l, r  
            while i < j:
                # 小的放左边, 大的放右边
                while strs[j] + strs[l] >= strs[l] + strs[j] and i < j:  # 自定义排序规则
                    j -= 1
                while strs[i] + strs[l] <= strs[l] + strs[i] and i < j:  # 自定义排序规则
                    i += 1
                # 交换, 使左小右大
                strs[i], strs[j] = strs[j], strs[i]
            # 跳出, 中间位置与首个位置元素交换
            strs[i], strs[l] = strs[l], strs[i]
            # 左、右子集快排
            quick_sort(l, i-1)
            quick_sort(i+1, r)
            
        # int -> str
        strs = [str(num) for num in nums]  
        # 基于自定义规则的快排
        quick_sort(0, len(strs)-1)  
        
        return ''.join(strs)

# 36ms - 92.48%
class Solution:
    def minNumber(self, nums: List[int]) -> str:
        def sort_rule(x, y):
            a, b = x + y, y + x
            if a > b: 
                return 1
            elif a < b: 
                return -1
            else: 
                return 0
        
        strs = [str(num) for num in nums]
        strs.sort(key = functools.cmp_to_key(sort_rule))  # 根据自定义规则排序
        return ''.join(strs)

3.3 解答

参考资料:

《剑指 Offer 第二版》

https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/59ypcj/

https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/59ceyt/


四、剑指 Offer 61. 扑克牌中的顺子

4.1 题求

4.2 求解

法一:排序 + 规则

# 20ms - 99.84%
class Solution:
    def isStraight(self, nums: List[int]) -> bool:
        # 排序
        nums.sort()
        # 相邻数值之差 (>1) 计数器
        diff_sum = 0
        for i in range(4, 0, -1):  # i = 4, 3, 2, 1
            # 若减数不为 0
            if nums[i-1] != 0:
                # 若被减数和减数均非 0, 则因重复不是顺子
                if nums[i] == nums[i-1]:
                    return False
                # 若相邻数值之差大于 1, 则非顺子, 累积差值
                diff = nums[i] - nums[i-1]
                if diff > 1:
                    diff_sum += diff
            # 若减数为 0
            else:
                # 若顺子差的牌数 (diff_sum//2) 小于大/小王 (0) 的个数, 则可以构成顺子
                return (diff_sum // 2) <= i
        # 牌中未出现大/小王 (0), 是否构成顺子取决于是否存在相邻数值之差大于 1
        return diff_sum == 0

官方说明 

# 28ms - 95.53%
class Solution:
    def isStraight(self, nums: List[int]) -> bool:
        repeat = set()
        ma, mi = 0, 14
        for num in nums:
            if num == 0:  # 跳过大小王
                continue 
            ma = max(ma, num)  # 最大牌
            mi = min(mi, num)  # 最小牌
            if num in repeat:  # 若有重复,提前返回 false
                return False  
            repeat.add(num)  # 添加牌至 Set
        return ma - mi < 5  # 最大牌 - 最小牌 < 5 则可构成顺子 

# 32ms - 86.18%
class Solution:
    def isStraight(self, nums: List[int]) -> bool:
        joker = 0  # 最小牌起点 / joker 数
        nums.sort()  # 数组排序
        for i in range(4):
            if nums[i] == 0: 
                joker += 1  # 统计大小王数量
            elif nums[i] == nums[i+1]: 
                return False  # 若有重复,提前返回 false
        return nums[4] - nums[joker] < 5  # 最大牌 - 最小牌 < 5 则可构成顺子

4.3 解答

参考资料:

《剑指 Offer 第二版》

https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/57mpoj/

https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/572x9r/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值