跳至主要內容

排序

绳子大约 8 分钟算法排序Leetcode

基础

冒泡排序

def bubble_sort(A):
    for i in range(len(A)):
        for j in range(0, len(A) - i - 1):
            if A[j] > A[j + 1]:
                A[j], A[j + 1] = A[j + 1], A[j]

插入排序

def insert_sort(A):
    for i in range(1, len(A)):
        temp = A[i]
        j = i - 1
        while j >= 0 and A[j] > temp:
            A[j + 1] = A[j]
            j -= 1
        A[j + 1] = temp  # 不满条件的下一个位置

选择排序

def select_sort(A):
    for i in range(0, len(A)):
        k = i
        # Find the smallest num and record its index
        for j in range(i, len(A)):
            if A[j] < A[k]:
                k = j
        if k != i:
            A[i], A[k] = A[k], A[i]

归并排序

1)返回数组法。

代码如下:

def merge(A, B):
    # return sorted(A + B)
    temp = []
    i, j = 0, 0
    while i < len(A) and j < len(B):
        if A[i] <= B[j]:
            temp.append(A[i])
            i += 1
        else:
            temp.append(B[j])
            j += 1
    temp.extend(A[i:])
    temp.extend(B[j:])
    return temp


def merge_sort(A):
    if len(A) <= 1:
        return A
    mid = len(A) // 2
    left = merge_sort(A[:mid])
    right = merge_sort(A[mid:])
    return merge(left, right)
    
# res = merge_sort(A)

2)原地操作法。函数参数稍微复杂。

def merge(A, left, mid, right):
    """A[left:mid+1], A[mid+1:right+1]"""
    temp = []  # 用于存储数字
    i, j = left, mid + 1
    while i <= mid and j <= right:
        if A[i] <= A[j]:
            temp.append(A[i])
            i += 1
        else:
            temp.append(A[j])
            j += 1
    temp.extend(A[i: mid + 1])
    temp.extend(A[j: right + 1])
    A[left: right + 1] = temp


def merge_sort(A, left, right):
    if left < right:
        mid = (left + right) // 2
        merge_sort(A, left, mid)
        merge_sort(A, mid + 1, right)
        merge(A, left, mid, right)
        
# merge_sort(A, 0, len(A) - 1)

快速排序

介绍三种写法的快速排序。三种写法的quick_sort函数相同,在此给出。

推荐使用填坑法,比较直观,便于记忆。

def quick_sort(A, left, right):
    if left < right:
        p = partition(A, left, right)
        quick_sort(A, left, p - 1)
        quick_sort(A, p + 1, right)
        
# quick_sort(A, 0, len(A) - 1)

1)填坑法。

使用第一个元素当作轴元素。注意先从右往左比较,大于等于号;再从左往右比较,小于号。

def partition(A, left, right):
    pivot = A[left]
    while left < right:
        while left < right and A[right] >= pivot:
            right -= 1
        A[left] = A[right]  # 看作填坑
        while left < right and A[left] < pivot:
            left += 1
        A[right] = A[left]  # 看作填坑
    A[left] = pivot
    return left

2)交换法。

使用第一个元素当作轴元素。注意先从右往左比较,大于等于号;再从左往右比较,小于等于号。

def partition(A, left, right):
    pivot_idx = left
    pivot = A[left]
    while left < right:
        while left < right and A[right] >= pivot:
            right -= 1
        while left < right and A[left] <= pivot:
            left += 1
        if left < right:  # 进行交换
            A[left], A[right] = A[right], A[left]
    A[pivot_idx], A[left] = A[left], A[pivot_idx]
    return left

3)顺序遍历法。

算法导论中的写法,选择最后一个元素作为轴元素。

def partition(A, left, right):
    pivot = A[right]  # 选择最后一个元素作为轴元素
    store_idx = left - 1
    for cur in range(left, right):  # 顺序遍历
        if A[cur] <= pivot:
            store_idx += 1
            A[store_idx], A[cur] = A[cur], A[store_idx]
    A[store_idx + 1], A[right] = A[right], A[store_idx + 1]
    return store_idx + 1

选择第一个元素作为轴元素,而且该写法可以推广到链表排序。

def partition(A, left, right):
    pivot = A[left]  # 选择第一个元素作为轴元素
    store_idx = left
    for cur in range(left + 1, right + 1):  # 顺序遍历
        if A[cur] <= pivot:
            store_idx += 1
            A[store_idx], A[cur] = A[cur], A[store_idx]
    A[left], A[store_idx] = A[store_idx], A[left]
    return store_idx

堆排序

Python实现 《算法导论 第三版》中的算法 第6章 堆排序open in new window

下面的代码实现了一个最大堆以及堆排序算法:

def get_parent(i):
    return (i - 1) // 2


def get_left(i):
    return 2 * i + 1


def get_right(i):
    return 2 * i + 2


def max_heapify_recursive(A, heap_size, i):
    l = get_left(i)
    r = get_right(i)
    largest_ind = i
    if l < heap_size and A[l] > A[largest_ind]:
        largest_ind = l
    if r < heap_size and A[r] > A[largest_ind]:
        largest_ind = r
    if largest_ind == i:
        return
    else:
        A[i], A[largest_ind] = A[largest_ind], A[i]
        max_heapify_recursive(A, heap_size, largest_ind)
    
    
def max_heapify_loop(A, heap_size, i): 
    while i < heap_size:
        l = get_left(i)
        r = get_right(i)
        largest_ind = i
        if l < heap_size and A[l] > A[largest_ind]:
            largest_ind = l
        if r < heap_size and A[r] > A[largest_ind]:
            largest_ind = r
        if largest_ind == i:
            break
        else:
            A[i], A[largest_ind] = A[largest_ind], A[i]
            i = largest_ind


def build_max_heap(A, heap_size):
    begin = len(A)//2 - 1  # len(A)//2 - 1是堆中第一个叶子节点的前一个节点
    for i in range(begin, -1, -1):
        max_heapify_loop(A, heap_size, i)


def heap_sort(A):
    heap_size = len(A)
    build_max_heap(A, heap_size)
    for i in range(len(A)-1, 0, -1):
        A[0], A[i] = A[i], A[0]  # 每次固定最后一个元素,并将堆大小减一
        heap_size -= 1
        max_heapify_loop(A, heap_size, 0)

线性时间排序

Python实现 《算法导论 第三版》中的算法 第8章 线性时间排序open in new window

上面的几种算法都是基于比较的算法,时间复杂度最好可以达到O(nlgn)O(n\lg n),比如归并排序、堆排序和快速排序。归并排序和堆排序在最坏情况下能够达到该复杂度,快速排序在平均情况达到该复杂度。注意,快速排序最坏情况下是O(n2)O(n^2)

下面介绍一下三种线性时间复杂度的排序算法:计数排序、基数排序和桶排序。

Leetcode 编程题

LCR 164. 把数组排成最小的数

LCR 164. 破解闯关密码open in new window

一、题目

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

二、解析

字典顺序。

代码如下:

class Solution:
    def minNumber(self, nums: List[int]) -> str:
        str_nums = [str(num) for num in nums]  
        for i in range(len(nums)):
            for j in range(len(nums) - i - 1):
                if str_nums[j] + str_nums[j + 1] > str_nums[j + 1] + str_nums[i]:
                    str_nums[j], str_nums[j + 1] = str_nums[j + 1], str_nums[j]
        
        return "".join(str_nums)

56. 合并区间

56. 合并区间open in new window

一、题目

给出一个区间的集合,请合并所有重叠的区间。

二、解析

排序即可。

代码如下:

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort(key=lambda ele: ele[0])
        res = []
        for inter in intervals:
            if res and res[-1][1] >= inter[0]:
                last = res.pop()
                res.append([last[0], max(last[1], inter[1])])
            else:
                res.append(inter)
        return res

406. 根据身高重建队列

406. 根据身高重建队列open in new window

一、题目

假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。 注意:总人数少于1100人。

二、解析

按身高降序、人数升序排序。

参考 Leetcode 官方题解open in new window

代码如下:

class Solution:
    def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
        people.sort(key = lambda x: (-x[0], x[1]))
        res = []
        for p in people:
            res.insert(p[1], p)
        return res

LCR 170. 交易逆序对的总数

LCR 170. 交易逆序对的总数open in new window

一、题目

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

二、解析

使用归并排序。

代码如下:

class Solution:
    def reversePairs(self, nums: List[int]) -> int:
        def merge(A, B):
            temp = []
            i, j = 0, 0
            while i < len(A) and j < len(B):
                if A[i] <= B[j]:
                    temp.append(A[i])
                    i += 1
                else:
                    temp.append(B[j])
                    j += 1
                    self.res += len(A) - i
            temp.extend(A[i:])
            temp.extend(B[j:])
            return temp

        def merge_sort(A):
            if len(A) <= 1:
                return A
            mid = len(A) // 2
            left = merge_sort(A[:mid])
            right = merge_sort(A[mid:])
            return merge(left, right)

        self.res = 0
        merge_sort(nums)
        return self.res

493. 翻转对

493. 翻转对open in new window

一、题目

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

二、解析

代码如下:

class Solution:
    def reversePairs(self, nums: List[int]) -> int:
        def merge(A, left, mid, right):
            """A[left:mid+1], A[mid+1:right+1]"""
            temp = []  # 用于存储数字
            i, j = left, mid + 1
            while i <= mid and j <= right:
                if A[i] <= A[j]:
                    temp.append(A[i])
                    i += 1
                else:
                    temp.append(A[j])
                    j += 1
            temp.extend(A[i: mid + 1])
            temp.extend(A[j: right + 1])
            A[left: right + 1] = temp


        def merge_sort(A, left, right):
            if left >= right:
                return 0
            mid = (left + right) // 2
            count = merge_sort(A, left, mid) + merge_sort(A, mid + 1, right)
            j = mid + 1
            for i in range(left, mid + 1):
                while j <= right and A[i] > A[j] * 2:
                    j += 1
                count += j - mid - 1
            merge(A, left, mid, right)
            return count
        
        return merge_sort(nums, 0, len(nums) - 1)

315. 计算右侧小于当前元素的个数

315. 计算右侧小于当前元素的个数open in new window

一、题目

给定一个整数数组 nums,按要求返回一个新数组counts。数组counts有该性质:counts[i] 的值是nums[i]右侧小于nums[i]的元素的数量。

二、解析

二分查找、归并排序、树状数组等。

1)二分查找

class Solution:
    def countSmaller(self, nums: List[int]) -> List[int]:
        import bisect
        queue = []
        res = []
        for num in nums[::-1]:
            loc = bisect.bisect_left(queue, num)
            res.append(loc)
            queue.insert(loc, num)
        return res[::-1]

2)归并排序

class Solution:
    def countSmaller(self, nums: List[int]) -> List[int]:
        def merge(left, right):
            temp = []
            i = j = 0
            while i < len(left) or j < len(right):
                if j == len(right) or i < len(left) and left[i][1] <= right[j][1]:
                    temp.append(left[i])
                    res[left[i][0]] += j
                    i += 1
                else:
                    temp.append(right[j])
                    j += 1
            return temp
        
        def merge_sort(nums):
            if len(nums) <= 1:
                return nums
            mid = len(nums) // 2
            left = merge_sort(nums[:mid])
            right = merge_sort(nums[mid:])
            return merge(left, right)
        
        res = [0] * len(nums)
        arr = [[i, num] for i, num in enumerate(nums)]
        merge_sort(arr)
        return res

3)树状数组

TODO

327. 区间和的个数

327. 区间和的个数open in new window

一、题目

给定一个整数数组nums,返回区间和在[lower,upper]之间的个数,包含lower和upper。区间和S(i, j)表示在nums中,位置从i到j的元素之和,包含i和j(i≤j)。说明: 最直观的算法复杂度是O(n^2),请在此基础上优化你的算法。

二、解析

TODO