HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 技术面试完全指南

    • 技术面试完全指南
    • 8年面试官告诉你:90%的简历在第一轮就被刷掉了
    • 刷了500道LeetCode,终于明白大厂算法面试到底考什么
    • 高频算法题精讲-双指针与滑动窗口
    • 03-高频算法题精讲-二分查找与排序
    • 04-高频算法题精讲-树与递归
    • 05-高频算法题精讲-图与拓扑排序
    • 06-高频算法题精讲-动态规划
    • Go面试必问:一道GMP问题,干掉90%的候选人
    • 08-数据库面试高频题
    • 09-分布式系统面试题
    • 10-Kubernetes与云原生面试题
    • 11-系统设计面试方法论
    • 前端面试高频题
    • AI 与机器学习面试题
    • 行为面试与软技能

03-高频算法题精讲-二分查找与排序

本章概述

二分查找和排序算法是面试中最基础也最重要的知识点。掌握二分查找的各种变体和经典排序算法,不仅能解决大量实际问题,还能在面试中展现扎实的算法功底。

本章涵盖:

  • 二分查找的4种模板及其应用场景
  • 快速排序、归并排序、堆排序的实现与优化
  • 15+道LeetCode高频真题详解
  • 完整的代码实现和复杂度分析

第一节:二分查找核心模板

1.1 二分查找的本质

二分查找不仅仅是在有序数组中查找元素,其本质是在单调性空间中寻找边界。只要能定义出"判定函数",就可以使用二分查找。

1.2 模板一:查找精确值

def binary_search(nums: list[int], target: int) -> int:
    """
    在有序数组中查找目标值
    返回目标值的索引,不存在返回-1
    """
    left, right = 0, len(nums) - 1

    while left <= right:
        mid = left + (right - left) // 2  # 防止溢出

        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1

    return -1

# 时间复杂度: O(log n)
# 空间复杂度: O(1)

Go语言实现:

func binarySearch(nums []int, target int) int {
    left, right := 0, len(nums)-1

    for left <= right {
        mid := left + (right-left)/2

        if nums[mid] == target {
            return mid
        } else if nums[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }

    return -1
}

1.3 模板二:查找左边界

查找第一个大于等于target的位置。

def binary_search_left(nums: list[int], target: int) -> int:
    """
    查找第一个大于等于target的位置
    """
    left, right = 0, len(nums)

    while left < right:
        mid = left + (right - left) // 2

        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid

    return left

# 时间复杂度: O(log n)
# 空间复杂度: O(1)

1.4 模板三:查找右边界

查找最后一个小于等于target的位置。

def binary_search_right(nums: list[int], target: int) -> int:
    """
    查找最后一个小于等于target的位置
    """
    left, right = 0, len(nums)

    while left < right:
        mid = left + (right - left) // 2

        if nums[mid] <= target:
            left = mid + 1
        else:
            right = mid

    return left - 1

# 时间复杂度: O(log n)
# 空间复杂度: O(1)

1.5 模板四:实数二分

def binary_search_real(left: float, right: float,
                       check: callable, eps: float = 1e-6) -> float:
    """
    实数域上的二分查找
    left: 左边界
    right: 右边界
    check: 判定函数
    eps: 精度要求
    """
    while right - left > eps:
        mid = (left + right) / 2

        if check(mid):
            right = mid
        else:
            left = mid

    return left

# 时间复杂度: O(log(n/eps))
# 空间复杂度: O(1)

第二节:二分查找经典题目

2.1 LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置(中等)

题目描述: 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

解题思路: 使用两次二分查找,分别找左边界和右边界。

def searchRange(nums: list[int], target: int) -> list[int]:
    """
    查找目标值的起始和结束位置
    """
    def find_left(nums, target):
        left, right = 0, len(nums)
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < target:
                left = mid + 1
            else:
                right = mid
        return left

    def find_right(nums, target):
        left, right = 0, len(nums)
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] <= target:
                left = mid + 1
            else:
                right = mid
        return left - 1

    if not nums:
        return [-1, -1]

    left_idx = find_left(nums, target)

    # 目标值不存在
    if left_idx == len(nums) or nums[left_idx] != target:
        return [-1, -1]

    right_idx = find_right(nums, target)

    return [left_idx, right_idx]

# 测试
print(searchRange([5,7,7,8,8,10], 8))  # [3, 4]
print(searchRange([5,7,7,8,8,10], 6))  # [-1, -1]

# 时间复杂度: O(log n)
# 空间复杂度: O(1)

Go语言实现:

func searchRange(nums []int, target int) []int {
    findLeft := func(nums []int, target int) int {
        left, right := 0, len(nums)
        for left < right {
            mid := left + (right-left)/2
            if nums[mid] < target {
                left = mid + 1
            } else {
                right = mid
            }
        }
        return left
    }

    findRight := func(nums []int, target int) int {
        left, right := 0, len(nums)
        for left < right {
            mid := left + (right-left)/2
            if nums[mid] <= target {
                left = mid + 1
            } else {
                right = mid
            }
        }
        return left - 1
    }

    if len(nums) == 0 {
        return []int{-1, -1}
    }

    leftIdx := findLeft(nums, target)
    if leftIdx == len(nums) || nums[leftIdx] != target {
        return []int{-1, -1}
    }

    rightIdx := findRight(nums, target)
    return []int{leftIdx, rightIdx}
}

2.2 LeetCode 33. 搜索旋转排序数组(中等)

题目描述: 整数数组 nums 按升序排列,数组中的值互不相同。在传递给函数之前,nums 在某个下标 k 进行了旋转。例如,[0,1,2,4,5,6,7] 在下标 3 处旋转后变为 [4,5,6,7,0,1,2]。

解题思路: 旋转后的数组可以分为两部分有序数组。通过判断mid在哪一部分,来决定搜索方向。

def search(nums: list[int], target: int) -> int:
    """
    在旋转排序数组中搜索目标值
    """
    left, right = 0, len(nums) - 1

    while left <= right:
        mid = left + (right - left) // 2

        if nums[mid] == target:
            return mid

        # 判断哪一部分是有序的
        if nums[left] <= nums[mid]:  # 左半部分有序
            if nums[left] <= target < nums[mid]:
                right = mid - 1
            else:
                left = mid + 1
        else:  # 右半部分有序
            if nums[mid] < target <= nums[right]:
                left = mid + 1
            else:
                right = mid - 1

    return -1

# 测试
print(search([4,5,6,7,0,1,2], 0))  # 4
print(search([4,5,6,7,0,1,2], 3))  # -1

# 时间复杂度: O(log n)
# 空间复杂度: O(1)

2.3 LeetCode 153. 寻找旋转排序数组中的最小值(中等)

题目描述: 假设按照升序排序的数组在某个点上进行了旋转,找出其中最小的元素。

解题思路: 最小值一定在右侧无序的部分。通过比较mid和right的值来缩小范围。

def findMin(nums: list[int]) -> int:
    """
    查找旋转数组中的最小值
    """
    left, right = 0, len(nums) - 1

    while left < right:
        mid = left + (right - left) // 2

        # mid在左侧有序部分,最小值在右侧
        if nums[mid] > nums[right]:
            left = mid + 1
        else:  # mid在右侧,最小值可能是mid或在左侧
            right = mid

    return nums[left]

# 测试
print(findMin([3,4,5,1,2]))  # 1
print(findMin([4,5,6,7,0,1,2]))  # 0

# 时间复杂度: O(log n)
# 空间复杂度: O(1)

2.4 LeetCode 69. x 的平方根(简单)

题目描述: 实现 int sqrt(int x) 函数,计算并返回 x 的平方根。

解题思路: 在 [0, x] 范围内二分查找,找到最大的 k 使得 k^2 <= x。

def mySqrt(x: int) -> int:
    """
    计算平方根的整数部分
    """
    if x == 0:
        return 0

    left, right = 1, x

    while left <= right:
        mid = left + (right - left) // 2

        if mid * mid == x:
            return mid
        elif mid * mid < x:
            left = mid + 1
        else:
            right = mid - 1

    return right  # 返回小于等于的最大值

# 测试
print(mySqrt(4))  # 2
print(mySqrt(8))  # 2

# 时间复杂度: O(log n)
# 空间复杂度: O(1)

2.5 LeetCode 162. 寻找峰值(中等)

题目描述: 峰值元素是指其值大于左右相邻值的元素。给定一个数组 nums,找到峰值元素并返回其索引。

解题思路: 通过比较mid和mid+1,决定往哪个方向搜索。如果nums[mid] < nums[mid+1],说明右侧一定有峰值。

def findPeakElement(nums: list[int]) -> int:
    """
    查找峰值元素
    """
    left, right = 0, len(nums) - 1

    while left < right:
        mid = left + (right - left) // 2

        if nums[mid] < nums[mid + 1]:
            # 右侧一定有峰值
            left = mid + 1
        else:
            # 左侧一定有峰值(包括mid)
            right = mid

    return left

# 测试
print(findPeakElement([1,2,3,1]))  # 2
print(findPeakElement([1,2,1,3,5,6,4]))  # 1 或 5

# 时间复杂度: O(log n)
# 空间复杂度: O(1)

2.6 LeetCode 875. 爱吃香蕉的珂珂(中等)

题目描述: 珂珂喜欢吃香蕉。有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。返回她可以在 h 小时内吃掉所有香蕉的最小速度 k。

解题思路: 二分查找速度k,判断是否能在h小时内吃完。

def minEatingSpeed(piles: list[int], h: int) -> int:
    """
    查找最小吃香蕉速度
    """
    def can_finish(speed):
        """判断以speed速度能否在h小时内吃完"""
        hours = 0
        for pile in piles:
            hours += (pile + speed - 1) // speed  # 向上取整
        return hours <= h

    left, right = 1, max(piles)

    while left < right:
        mid = left + (right - left) // 2

        if can_finish(mid):
            right = mid  # 可以吃完,尝试更慢的速度
        else:
            left = mid + 1  # 吃不完,需要更快的速度

    return left

# 测试
print(minEatingSpeed([3,6,7,11], 8))  # 4
print(minEatingSpeed([30,11,23,4,20], 5))  # 30

# 时间复杂度: O(n log m),其中m是最大堆的大小
# 空间复杂度: O(1)

2.7 LeetCode 4. 寻找两个正序数组的中位数(困难)

题目描述: 给定两个大小分别为 m 和 n 的正序数组 nums1 和 nums2。请找出这两个正序数组的中位数。

解题思路: 在较短的数组上进行二分查找,找到合适的分割点,使得左半部分的最大值小于等于右半部分的最小值。

def findMedianSortedArrays(nums1: list[int], nums2: list[int]) -> float:
    """
    查找两个有序数组的中位数
    """
    # 确保nums1是较短的数组
    if len(nums1) > len(nums2):
        nums1, nums2 = nums2, nums1

    m, n = len(nums1), len(nums2)
    left, right = 0, m

    while left <= right:
        # nums1的分割点
        partition1 = (left + right) // 2
        # nums2的分割点
        partition2 = (m + n + 1) // 2 - partition1

        # 左半部分的最大值
        max_left1 = float('-inf') if partition1 == 0 else nums1[partition1 - 1]
        max_left2 = float('-inf') if partition2 == 0 else nums2[partition2 - 1]

        # 右半部分的最小值
        min_right1 = float('inf') if partition1 == m else nums1[partition1]
        min_right2 = float('inf') if partition2 == n else nums2[partition2]

        # 找到了正确的分割点
        if max_left1 <= min_right2 and max_left2 <= min_right1:
            # 总长度为偶数
            if (m + n) % 2 == 0:
                return (max(max_left1, max_left2) + min(min_right1, min_right2)) / 2
            # 总长度为奇数
            else:
                return max(max_left1, max_left2)
        # nums1分割点太靠右
        elif max_left1 > min_right2:
            right = partition1 - 1
        # nums1分割点太靠左
        else:
            left = partition1 + 1

    return 0.0

# 测试
print(findMedianSortedArrays([1,3], [2]))  # 2.0
print(findMedianSortedArrays([1,2], [3,4]))  # 2.5

# 时间复杂度: O(log(min(m,n)))
# 空间复杂度: O(1)

第三节:快速排序

3.1 快速排序原理

快速排序是一种分治算法,通过选择一个基准元素(pivot),将数组分为两部分:小于基准的元素和大于基准的元素,然后递归地对两部分进行排序。

核心思想:

  1. 选择基准元素
  2. 分区(Partition):将小于基准的元素移到左边,大于基准的移到右边
  3. 递归排序左右两部分

3.2 快速排序实现

def quickSort(arr: list[int]) -> list[int]:
    """
    快速排序
    """
    def partition(arr, low, high):
        """分区函数"""
        pivot = arr[high]  # 选择最后一个元素作为基准
        i = low - 1  # i指向小于pivot的最后一个元素

        for j in range(low, high):
            if arr[j] <= pivot:
                i += 1
                arr[i], arr[j] = arr[j], arr[i]

        arr[i + 1], arr[high] = arr[high], arr[i + 1]
        return i + 1

    def quick_sort_helper(arr, low, high):
        if low < high:
            # 获取分区点
            pi = partition(arr, low, high)

            # 递归排序左右两部分
            quick_sort_helper(arr, low, pi - 1)
            quick_sort_helper(arr, pi + 1, high)

    quick_sort_helper(arr, 0, len(arr) - 1)
    return arr

# 测试
print(quickSort([64, 34, 25, 12, 22, 11, 90]))
# [11, 12, 22, 25, 34, 64, 90]

# 时间复杂度: 平均O(n log n),最坏O(n^2)
# 空间复杂度: O(log n)递归栈空间

Go语言实现:

func quickSort(arr []int) []int {
    partition := func(arr []int, low, high int) int {
        pivot := arr[high]
        i := low - 1

        for j := low; j < high; j++ {
            if arr[j] <= pivot {
                i++
                arr[i], arr[j] = arr[j], arr[i]
            }
        }

        arr[i+1], arr[high] = arr[high], arr[i+1]
        return i + 1
    }

    var quickSortHelper func([]int, int, int)
    quickSortHelper = func(arr []int, low, high int) {
        if low < high {
            pi := partition(arr, low, high)
            quickSortHelper(arr, low, pi-1)
            quickSortHelper(arr, pi+1, high)
        }
    }

    quickSortHelper(arr, 0, len(arr)-1)
    return arr
}

3.3 三路快排优化

当数组中有大量重复元素时,三路快排性能更好。

def threeWayQuickSort(arr: list[int]) -> list[int]:
    """
    三路快排:将数组分为小于、等于、大于pivot三部分
    """
    def three_way_partition(arr, low, high):
        if low >= high:
            return

        pivot = arr[low]
        lt = low  # arr[low+1...lt] < pivot
        gt = high  # arr[gt...high] > pivot
        i = low + 1  # arr[lt+1...i-1] == pivot

        while i <= gt:
            if arr[i] < pivot:
                arr[lt], arr[i] = arr[i], arr[lt]
                lt += 1
                i += 1
            elif arr[i] > pivot:
                arr[gt], arr[i] = arr[i], arr[gt]
                gt -= 1
            else:
                i += 1

        # 递归排序小于和大于pivot的部分
        three_way_partition(arr, low, lt - 1)
        three_way_partition(arr, gt + 1, high)

    three_way_partition(arr, 0, len(arr) - 1)
    return arr

# 测试
print(threeWayQuickSort([4, 2, 1, 3, 2, 4, 1, 3]))
# [1, 1, 2, 2, 3, 3, 4, 4]

# 时间复杂度: O(n log n)
# 空间复杂度: O(log n)

3.4 LeetCode 215. 数组中的第K个最大元素(中等)

题目描述: 在未排序的数组中找到第 k 个最大的元素。

解题思路: 使用快速选择算法,平均时间复杂度为O(n)。

def findKthLargest(nums: list[int], k: int) -> int:
    """
    查找第k个最大元素
    """
    def partition(left, right):
        pivot = nums[right]
        i = left

        for j in range(left, right):
            if nums[j] >= pivot:  # 降序排列
                nums[i], nums[j] = nums[j], nums[i]
                i += 1

        nums[i], nums[right] = nums[right], nums[i]
        return i

    left, right = 0, len(nums) - 1
    k_index = k - 1

    while left <= right:
        pivot_index = partition(left, right)

        if pivot_index == k_index:
            return nums[pivot_index]
        elif pivot_index < k_index:
            left = pivot_index + 1
        else:
            right = pivot_index - 1

    return -1

# 测试
print(findKthLargest([3,2,1,5,6,4], 2))  # 5
print(findKthLargest([3,2,3,1,2,4,5,5,6], 4))  # 4

# 时间复杂度: 平均O(n),最坏O(n^2)
# 空间复杂度: O(1)

第四节:归并排序

4.1 归并排序原理

归并排序是经典的分治算法,将数组分成两半,递归排序,然后合并两个有序数组。

核心步骤:

  1. 分解:将数组递归地分成两半
  2. 解决:递归地排序两个子数组
  3. 合并:将两个有序子数组合并成一个有序数组

4.2 归并排序实现

def mergeSort(arr: list[int]) -> list[int]:
    """
    归并排序
    """
    def merge(left, right):
        """合并两个有序数组"""
        result = []
        i = j = 0

        while i < len(left) and j < len(right):
            if left[i] <= right[j]:
                result.append(left[i])
                i += 1
            else:
                result.append(right[j])
                j += 1

        result.extend(left[i:])
        result.extend(right[j:])
        return result

    if len(arr) <= 1:
        return arr

    mid = len(arr) // 2
    left = mergeSort(arr[:mid])
    right = mergeSort(arr[mid:])

    return merge(left, right)

# 测试
print(mergeSort([64, 34, 25, 12, 22, 11, 90]))
# [11, 12, 22, 25, 34, 64, 90]

# 时间复杂度: O(n log n)
# 空间复杂度: O(n)

原地归并排序(节省空间):

def mergeSortInPlace(arr: list[int]) -> list[int]:
    """
    原地归并排序
    """
    def merge(arr, left, mid, right):
        """原地合并"""
        temp = []
        i, j = left, mid + 1

        while i <= mid and j <= right:
            if arr[i] <= arr[j]:
                temp.append(arr[i])
                i += 1
            else:
                temp.append(arr[j])
                j += 1

        temp.extend(arr[i:mid+1])
        temp.extend(arr[j:right+1])

        # 复制回原数组
        for i, val in enumerate(temp):
            arr[left + i] = val

    def merge_sort_helper(arr, left, right):
        if left < right:
            mid = left + (right - left) // 2
            merge_sort_helper(arr, left, mid)
            merge_sort_helper(arr, mid + 1, right)
            merge(arr, left, mid, right)

    merge_sort_helper(arr, 0, len(arr) - 1)
    return arr

# 时间复杂度: O(n log n)
# 空间复杂度: O(n)临时数组

4.3 LeetCode 88. 合并两个有序数组(简单)

题目描述: 给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。

解题思路: 从后往前填充,避免覆盖nums1中的元素。

def merge(nums1: list[int], m: int, nums2: list[int], n: int) -> None:
    """
    合并两个有序数组
    """
    # 从后往前填充
    p1 = m - 1  # nums1的指针
    p2 = n - 1  # nums2的指针
    p = m + n - 1  # 合并后的指针

    while p1 >= 0 and p2 >= 0:
        if nums1[p1] > nums2[p2]:
            nums1[p] = nums1[p1]
            p1 -= 1
        else:
            nums1[p] = nums2[p2]
            p2 -= 1
        p -= 1

    # 如果nums2还有剩余
    nums1[:p2+1] = nums2[:p2+1]

# 测试
nums1 = [1,2,3,0,0,0]
merge(nums1, 3, [2,5,6], 3)
print(nums1)  # [1, 2, 2, 3, 5, 6]

# 时间复杂度: O(m + n)
# 空间复杂度: O(1)

4.4 LeetCode 148. 排序链表(中等)

题目描述: 给定链表的头结点 head ,请将其按升序排列并返回排序后的链表。

解题思路: 使用归并排序,先找到中点分割链表,然后递归排序并合并。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def sortList(head: ListNode) -> ListNode:
    """
    链表归并排序
    """
    if not head or not head.next:
        return head

    # 快慢指针找中点
    slow, fast = head, head.next
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next

    # 分割链表
    mid = slow.next
    slow.next = None

    # 递归排序
    left = sortList(head)
    right = sortList(mid)

    # 合并两个有序链表
    dummy = ListNode(0)
    curr = dummy

    while left and right:
        if left.val < right.val:
            curr.next = left
            left = left.next
        else:
            curr.next = right
            right = right.next
        curr = curr.next

    curr.next = left if left else right

    return dummy.next

# 时间复杂度: O(n log n)
# 空间复杂度: O(log n)递归栈空间

4.5 LeetCode 493. 翻转对(困难)

题目描述: 给定一个数组 nums,如果 i < j 且 nums[i] > 2*nums[j],则称 (i, j) 为一个重要翻转对。返回给定数组中的重要翻转对的数量。

解题思路: 在归并排序的过程中统计翻转对数量。

def reversePairs(nums: list[int]) -> int:
    """
    统计翻转对数量
    """
    def merge_sort(arr):
        if len(arr) <= 1:
            return arr, 0

        mid = len(arr) // 2
        left, left_count = merge_sort(arr[:mid])
        right, right_count = merge_sort(arr[mid:])

        # 统计跨越左右两部分的翻转对
        cross_count = 0
        j = 0
        for i in range(len(left)):
            while j < len(right) and left[i] > 2 * right[j]:
                j += 1
            cross_count += j

        # 合并
        merged = []
        i = j = 0
        while i < len(left) and j < len(right):
            if left[i] <= right[j]:
                merged.append(left[i])
                i += 1
            else:
                merged.append(right[j])
                j += 1

        merged.extend(left[i:])
        merged.extend(right[j:])

        return merged, left_count + right_count + cross_count

    _, count = merge_sort(nums)
    return count

# 测试
print(reversePairs([1,3,2,3,1]))  # 2
print(reversePairs([2,4,3,5,1]))  # 3

# 时间复杂度: O(n log n)
# 空间复杂度: O(n)

第五节:堆排序

5.1 堆的基本概念

堆是一种特殊的完全二叉树,分为最大堆和最小堆:

  • 最大堆:每个节点的值都大于等于其子节点的值
  • 最小堆:每个节点的值都小于等于其子节点的值

数组表示堆:

  • 父节点索引:(i - 1) // 2
  • 左子节点索引:2 * i + 1
  • 右子节点索引:2 * i + 2

5.2 堆排序实现

def heapSort(arr: list[int]) -> list[int]:
    """
    堆排序
    """
    def heapify(arr, n, i):
        """
        调整堆,使以i为根的子树满足最大堆性质
        n: 堆的大小
        i: 当前节点索引
        """
        largest = i
        left = 2 * i + 1
        right = 2 * i + 2

        # 找到最大值的索引
        if left < n and arr[left] > arr[largest]:
            largest = left

        if right < n and arr[right] > arr[largest]:
            largest = right

        # 如果最大值不是根节点,交换并继续调整
        if largest != i:
            arr[i], arr[largest] = arr[largest], arr[i]
            heapify(arr, n, largest)

    n = len(arr)

    # 构建最大堆(从最后一个非叶子节点开始)
    for i in range(n // 2 - 1, -1, -1):
        heapify(arr, n, i)

    # 一个个取出堆顶元素
    for i in range(n - 1, 0, -1):
        arr[0], arr[i] = arr[i], arr[0]  # 交换
        heapify(arr, i, 0)  # 调整剩余堆

    return arr

# 测试
print(heapSort([64, 34, 25, 12, 22, 11, 90]))
# [11, 12, 22, 25, 34, 64, 90]

# 时间复杂度: O(n log n)
# 空间复杂度: O(1)

Go语言实现:

func heapSort(arr []int) []int {
    heapify := func(arr []int, n, i int) {
        largest := i
        left := 2*i + 1
        right := 2*i + 2

        if left < n && arr[left] > arr[largest] {
            largest = left
        }

        if right < n && arr[right] > arr[largest] {
            largest = right
        }

        if largest != i {
            arr[i], arr[largest] = arr[largest], arr[i]
            heapify(arr, n, largest)
        }
    }

    n := len(arr)

    // 构建最大堆
    for i := n/2 - 1; i >= 0; i-- {
        heapify(arr, n, i)
    }

    // 排序
    for i := n - 1; i > 0; i-- {
        arr[0], arr[i] = arr[i], arr[0]
        heapify(arr, i, 0)
    }

    return arr
}

5.3 LeetCode 347. 前 K 个高频元素(中等)

题目描述: 给定一个整数数组 nums 和一个整数 k,返回出现频率前 k 高的元素。

解题思路: 使用最小堆维护频率最高的k个元素。

import heapq
from collections import Counter

def topKFrequent(nums: list[int], k: int) -> list[int]:
    """
    查找前k个高频元素
    """
    # 统计频率
    count = Counter(nums)

    # 使用最小堆维护前k个高频元素
    heap = []

    for num, freq in count.items():
        heapq.heappush(heap, (freq, num))
        if len(heap) > k:
            heapq.heappop(heap)

    return [num for freq, num in heap]

# 测试
print(topKFrequent([1,1,1,2,2,3], 2))  # [1, 2]
print(topKFrequent([1], 1))  # [1]

# 时间复杂度: O(n log k)
# 空间复杂度: O(n)

方法二:使用快速选择(更优解)

def topKFrequent(nums: list[int], k: int) -> list[int]:
    """
    使用快速选择算法
    """
    count = Counter(nums)
    unique = list(count.keys())

    def partition(left, right):
        pivot_freq = count[unique[right]]
        i = left

        for j in range(left, right):
            if count[unique[j]] < pivot_freq:
                unique[i], unique[j] = unique[j], unique[i]
                i += 1

        unique[i], unique[right] = unique[right], unique[i]
        return i

    def quickselect(left, right, k_smallest):
        if left == right:
            return

        pivot_index = partition(left, right)

        if k_smallest == pivot_index:
            return
        elif k_smallest < pivot_index:
            quickselect(left, pivot_index - 1, k_smallest)
        else:
            quickselect(pivot_index + 1, right, k_smallest)

    n = len(unique)
    quickselect(0, n - 1, n - k)

    return unique[n - k:]

# 时间复杂度: O(n)平均情况
# 空间复杂度: O(n)

5.4 LeetCode 295. 数据流的中位数(困难)

题目描述: 中位数是有序列表中间的数。设计一个支持以下两种操作的数据结构:

  • void addNum(int num) - 从数据流中添加一个整数到数据结构中。
  • double findMedian() - 返回目前所有元素的中位数。

解题思路: 使用两个堆:最大堆存储较小的一半,最小堆存储较大的一半。

import heapq

class MedianFinder:
    """
    数据流中位数查找器
    """
    def __init__(self):
        # 最大堆(存储较小的一半)
        self.small = []
        # 最小堆(存储较大的一半)
        self.large = []

    def addNum(self, num: int) -> None:
        # 先加入最大堆
        heapq.heappush(self.small, -num)

        # 将最大堆的最大元素移到最小堆
        heapq.heappush(self.large, -heapq.heappop(self.small))

        # 平衡两个堆的大小
        if len(self.large) > len(self.small):
            heapq.heappush(self.small, -heapq.heappop(self.large))

    def findMedian(self) -> float:
        if len(self.small) > len(self.large):
            return -self.small[0]
        return (-self.small[0] + self.large[0]) / 2.0

# 测试
mf = MedianFinder()
mf.addNum(1)
mf.addNum(2)
print(mf.findMedian())  # 1.5
mf.addNum(3)
print(mf.findMedian())  # 2.0

# addNum时间复杂度: O(log n)
# findMedian时间复杂度: O(1)
# 空间复杂度: O(n)

第六节:其他经典排序题目

6.1 LeetCode 912. 排序数组(中等)

题目描述: 给定一个整数数组 nums,将该数组升序排列。

解题思路: 可以使用快速排序、归并排序或堆排序。这里展示随机化快排。

import random

def sortArray(nums: list[int]) -> list[int]:
    """
    随机化快速排序
    """
    def partition(left, right):
        # 随机选择pivot
        pivot_idx = random.randint(left, right)
        nums[pivot_idx], nums[right] = nums[right], nums[pivot_idx]

        pivot = nums[right]
        i = left

        for j in range(left, right):
            if nums[j] < pivot:
                nums[i], nums[j] = nums[j], nums[i]
                i += 1

        nums[i], nums[right] = nums[right], nums[i]
        return i

    def quicksort(left, right):
        if left < right:
            pi = partition(left, right)
            quicksort(left, pi - 1)
            quicksort(pi + 1, right)

    quicksort(0, len(nums) - 1)
    return nums

# 时间复杂度: O(n log n)平均
# 空间复杂度: O(log n)

6.2 LeetCode 75. 颜色分类(中等)

题目描述: 给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。用整数 0、1 和 2 分别表示红色、白色和蓝色。

解题思路: 使用三路快排的思想,维护三个区域。

def sortColors(nums: list[int]) -> None:
    """
    荷兰国旗问题
    """
    p0 = 0  # [0, p0)区域全是0
    p2 = len(nums) - 1  # (p2, len-1]区域全是2
    curr = 0

    while curr <= p2:
        if nums[curr] == 0:
            nums[p0], nums[curr] = nums[curr], nums[p0]
            p0 += 1
            curr += 1
        elif nums[curr] == 2:
            nums[curr], nums[p2] = nums[p2], nums[curr]
            p2 -= 1
            # curr不移动,因为交换来的元素还未检查
        else:
            curr += 1

# 测试
nums = [2,0,2,1,1,0]
sortColors(nums)
print(nums)  # [0, 0, 1, 1, 2, 2]

# 时间复杂度: O(n)
# 空间复杂度: O(1)

6.3 LeetCode 179. 最大数(中等)

题目描述: 给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。

解题思路: 自定义比较函数,比较 x+y 和 y+x 的大小。

from functools import cmp_to_key

def largestNumber(nums: list[int]) -> str:
    """
    组成最大数
    """
    # 转换为字符串
    nums_str = list(map(str, nums))

    # 自定义比较函数
    def compare(x, y):
        if x + y > y + x:
            return -1
        elif x + y < y + x:
            return 1
        else:
            return 0

    # 排序
    nums_str.sort(key=cmp_to_key(compare))

    # 拼接结果
    result = ''.join(nums_str)

    # 处理全0的情况
    return '0' if result[0] == '0' else result

# 测试
print(largestNumber([10,2]))  # "210"
print(largestNumber([3,30,34,5,9]))  # "9534330"

# 时间复杂度: O(n log n)
# 空间复杂度: O(n)

本章总结

本章详细讲解了二分查找和排序算法的核心知识:

二分查找要点:

  1. 掌握4种基本模板:精确查找、左边界、右边界、实数二分
  2. 理解二分查找的本质是在单调空间中寻找边界
  3. 注意边界条件的处理和循环不变量
  4. 灵活应用于旋转数组、峰值查找、速度二分等场景

排序算法要点:

  1. 快速排序:平均O(n log n),原地排序,不稳定

    • 三路快排优化处理重复元素
    • 随机化避免最坏情况
    • 快速选择算法求第K大元素
  2. 归并排序:O(n log n),稳定排序,需要额外空间

    • 适合链表排序
    • 可用于统计逆序对等问题
  3. 堆排序:O(n log n),原地排序,不稳定

    • 堆的应用:TopK问题、中位数查找
    • 优先队列的底层实现

复杂度对比:

算法平均时间最坏时间空间复杂度稳定性
快速排序O(n log n)O(n²)O(log n)不稳定
归并排序O(n log n)O(n log n)O(n)稳定
堆排序O(n log n)O(n log n)O(1)不稳定

下一章讲解树与递归算法,包括二叉树遍历、DFS、BFS和回溯算法。

Prev
高频算法题精讲-双指针与滑动窗口
Next
04-高频算法题精讲-树与递归