数据结构与算法----复习Part 4(数组排序)

本系列是算法通关手册LeeCode的学习笔记

算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn)

本系列为自用笔记,如有版权问题,请私聊我删除。

目录

数组排序

01. 冒泡排序(Bubble Sort)

02. 选择排序(Selection Sort)

03. 插入排序(Insertion Sort)

04. 希尔排序(Shell Sort)

05. 归并排序(Merge Sort)

06. 快速排序(Quick Sort)

07. 堆排序(Heap Sort)

7.1 堆结构

7.2 堆的存储结构

7.3 堆的操作

        访问堆顶元素

        向堆中插入元素

        删除堆顶元素

7.4 堆排序

08. 计数排序(Counting Sort)

09. 桶排序(Bucket Sort)

10. 基数排序(Radix Sort)

排序例题

总结


数组排序

01. 冒泡排序(Bubble Sort)

        算法思想:通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面。

        步骤:

        对前 n 个元素执行冒泡,从而使最大值的元素放在正确的位置上;

        对 n - 1 个元素执行冒泡,使第二大的元素放在正确的位置上;

        重复上述过程,直到数组有序。

代码实现:

class Solution:
    def bubbleSort(self, nums: [int]) -> [int]:
        for i in range(len(nums) - 1):
            flag = False      # 交换标志位
            for j in range(len(nums) - 1 - i):
                if nums[j] > nums[j + 1] :
                    nums[j], nums[j + 1] = nums[j + 1], nums[j]
                    flag = True # 发生交换
            if not flag:    # 未发生交换,此时已经有序
                break
        return nums
    def sortArray(self, nums: [int]) -> [int]:
        return self.bubbleSort(nums)

        嵌套循环中,每层执行次数都与数组规模 n 相关,时间复杂度为 O(n ^ 2)

        空间复杂度为 O(1) ,在原有的数组上进行操作,并未开辟新的内存。

02. 选择排序(Selection Sort)

        算法思想:将数组分为左右两个区间,左侧为有序区间,右侧为无序区间,每次选择无序区间中最小的数排在有序区间的末尾,直到无序区间为空。

        步骤:

        记录有序区间末尾的位置,从 0 开始;

        遍历无序区间,找到最小元素所在的位置,交换;

        有序区间加一,无序区间减一,直到无序区间为空。

代码实现:

    def selectionSort(self, nums: [int]) -> [int]:
        cnt = 0
        while cnt < len(nums) - 1:
            minPos = cnt
            minM = nums[cnt]
            for i in range(cnt, len(nums)):
                    if minM >= nums[i]:
                        minM = nums[i]
                        minPos = i
            nums[cnt], nums[minPos] = nums[minPos], nums[cnt]
            cnt += 1
        return nums

        时间复杂度为 O(n^2) 

        空间复杂度为 O(1)

03. 插入排序(Insertion Sort)

        算法思想:将数组分为左侧有序区间和右侧无序区间。每次从无序区间取出一个元素,然后将其插入到有序区间的适当位置。一个元素视为有序。

        步骤:

        取得无序区间第一个元素 a ;

        将其与有序区间的最后一个元素 b 比较,

                若 a > b 表示 a 是有序区间最大的元素;

                若 a < b ,将 b 向后移动一位,取得下一个 b 直到有序区间为空或 a > b.

        将 a 放入正确位置,有序区间加一, 无序区间减一。

代码实现:

    def insertSort(self, nums: [int]) -> [int]:
        for i in range(1, len(nums)):
            temp = nums[i]
            j = i
            while j > 0 and nums[j - 1] > temp:
                nums[j] = nums[j - 1]
                j -= 1
            nums[j] = temp
        return nums

        时间复杂度为 O(n^2) 

        空间复杂度为 O(1)

04. 希尔排序(Shell Sort)

        算法思想:整个数组按照一定的间隔取值划分为若干个子数组,并对子数组排序,直到间隔为一,对整个数组进行排序。

        步骤:

        确定一个元素间隔数 gap ;

        将所有间隔为 gap 的元素视为一个子数组;

        对子数组排序;

        减少间隔数,划分新的子数组,进行排序;

        直到间隔数 gap 为 1, 对整个数组排序。

以插入排序为例:

代码实现: 

    def shellSort(self, nums: [int]) -> [int]:
        size = len(nums)
        gap = size // 2
        while gap > 0 :
            # 以下类比插入排序
            for i in range(gap, size):
                temp = nums[i]
                j = i
                while j >= gap and nums[j - gap] > temp:
                    nums[j] = nums[j - gap]
                    j -= gap
                nums[j] = temp
            gap = gap // 2
        return nums

        曾有过疑问,希尔排序与插入排序的区别在哪

        可以看到,上述以 gap = n / 2 的间隔划分子数组,可以更快地使数组有序,而在最后一遍插入排序时,有序度高的数组执行交换的次数随之减少。这是依赖于间隔 gap 的函数,直接给出结论: 时间复杂度在 O(n * log ^ 2(n)) 与 O(n ^ 2) 之间。

        空间复杂度 O(1)

        特别要说明的是,与先前的排序算法不同,对于相同元素,可能在分组时分到不同组别,分别进行插入排序,最后合并时,可能改变相同元素的相对位置

        如 a = 1, b = 2, c = 1, 开始时 a 和 c 都为 1, 而 a 在 c 左侧

        排序后可能为 c, a, b 或 a, c, b

        虽然两种情况在数值上都是 1, 1, 2 ,实现了排序,但是c, a, b 中,a 和 c 的相对位置改变了,而上面三种不会。

        称这种为【不稳定排序算法】,而不改变相同元素相对位置的叫做【稳定排序算法】

        不特别说明的都是 稳定排序算法。

05. 归并排序(Merge Sort)

        算法思想:经典的分治策略,先递归地将当前数组分成两半,然后将有序数组两两合并,最终得到有序数组。

        步骤:

        二分当前数组,直到长度为 1;

        从长度为 1 的数组逐层合并,直到得到完整数组。

代码实现:

    def merge(self, left_nums: [int], right_nums: [int]) -> [int]:
        nums = []
        left_i, right_i = 0, 0
        while left_i < len(left_nums) and right_i < len(right_nums):
            # 将两个有序子数组中较小元素依次插入到结果数组中
            if left_nums[left_i] < right_nums[right_i]:
                nums.append(left_nums[left_i])
                left_i += 1
            else:
                nums.append(right_nums[right_i])
                right_i += 1

        # 如果左子数组有剩余元素,则将其插入到结果数组中
        while left_i < len(left_nums):
            nums.append(left_nums[left_i])
            left_i += 1

        # 如果右子数组有剩余元素,则将其插入到结果数组中
        while right_i < len(right_nums):
            nums.append(right_nums[right_i])
            right_i += 1
        # 返回合并后的结果数组
        return nums

    def mergeSort(self, nums: [int]) -> [int]:
        if len(nums) <= 1:
            return nums
        mid = len(nums) // 2
        left_nums = self.mergeSort(nums[ :mid])     # 接受了mergeSort的返回值
        right_nums = self.mergeSort(nums[mid: ])    # 而mergeSort的返回值是merge的返回值
        return self.merge(left_nums, right_nums)    # merge的返回值是新开辟的数组nums

    def sortArray(self, nums: [int]) -> [int]:
        return self.mergeSort(nums)

        时间复杂度为 O(n * logn)

        空间复杂度:每次调用 merge 都开辟了新数组,将其赋值给 left_nums 或right_nums 后,又释放,因此所需的最大额外空间为最后一次的 n,故空间复杂度为 O(n)

更详细的过程可看我写的另一篇:

Python数据结构与算法 算法基础6-CSDN博客

06. 快速排序(Quick Sort)

        算法思想:选择一个元素 a ,使得其左侧的元素都小于 a,其右侧的元素都大于 a,以同样的方式,将 a 左右的子数组进行排序,直到整个数组有序。

        步骤:

        选择一个基准数 pivot;

        先从右向左找到第一个小于 pivot  的数,记录其位置 j ;

        再从左向右找到第一个大于 pivot 的数, 记录其位置 i;

        交换位置 i,j  的元素,继续先从右向左找;

        当 i = j 时,找到基准数应在的位置;

        因为先找的是大于基准数 pivot 的元素,所以 i 位置一定是小于基准数的元素;

        所以交换后不影响左侧都是小于基准数这一效果。

        

                然后递归地处理 pivot 左右两侧的子数组

代码实现:

    def patition(self, nums: [int], low: int, high: int) -> int:
        pivot = nums[low]
        i = low
        j = high
        while i < j:
            while nums[j] >= pivot and i < j:
                j -= 1
            while nums[i] <= pivot and i < j:
                i += 1
            nums[i], nums[j] = nums[j], nums[i]
        nums[i], nums[low] = nums[low], nums[i]
        return i

    def quickSort(self, nums: [int], low: int, high: int) -> [int]:
        if low < high:
            mid = self.patition(nums, low, high)
            self.quickSort(nums, low, mid - 1)
            self.quickSort(nums, mid + 1, high)
        return nums

        时间复杂度:最坏情况下,原数组有序时,对于基准元素 pivot 的位置,j 需要从后向前遍历一遍,每次如此,且仅将子数组长度减 1 ,此时时间复杂度为 O(n ^ 2).

        平均情况下时间复杂度为 O(n * logn)

        空间复杂度为 O(n)

07. 堆排序(Heap Sort)

        进行堆排序之前,先大概了解堆的结构与操作。

7.1 堆结构

        堆(Heap),一种满足以下两个条件之一的完全二叉树:

                大顶(根)堆(Max Heap):任意节点值 >= 其子节点值;

                小顶(根)堆(Min Heap):任意节点值 <= 其子节点值;

7.2 堆的存储结构

        堆的逻辑结构就是一棵完全二叉树。

        当使用数组来表示堆时,堆中元素的节点编号与数组的索引关系为:

7.3 堆的操作

        访问堆顶元素

        从堆结构中获取位于堆顶的元素。

        在堆中,堆顶元素位于根节点,当我们使用数组来表示堆时,堆顶元素就是数组的首个元素

class Maxheap:
    ......
    def peek(self) -> int:
        # 大顶堆为空
        if not self.max_heap:
            return None
        return self.max_heap[0]

        不依赖于数组中元素个数,时间复杂度为 O(1)

        向堆中插入元素

        将一个新元素添加到堆中,调整堆的结构,以保持堆的特性不变。

        步骤:

        将新元素加到堆的末尾,保持完全二叉树结构;

        从插入的新节点开始,与父节点比较,若大于父节点则交换,保持大顶堆特性;

        直到不再大于父节点或到达根节点。

        该过程称为上移 : shift up

        

        

class Maxheap:
    max_heap = []

    def push(self, val: int):
        self.max_heap.append(val)

        size = len(self.max_heap)

        self.__shift_up(size - 1)

    def __shift_up(self, i: int):
        # (i - 1) // 2 为加入节点的父节点
        while (i - 1) // 2 >= 0 and self.max_heap[i] > self.max_heap[(i - 1) // 2] :
            self.max_heap[i], self.max_heap[(i - 1) // 2] = self.max_heap[(i - 1) // 2], self.max_heap[i]
            i = (i - 1) // 2

        在最坏情况下,时间复杂度为 O(log⁡n) ,其中 n 是堆中元素的数量,这是因为堆的高度是 log⁡n

        删除堆顶元素

        从堆中移除位于堆顶的元素,并重新调整堆的结构,以保持堆的特性不变。

        步骤:

        将堆的元素与末尾元素交换;

        将交换后的末尾元素移除;

        从新的堆顶开始,将其与较大的子节点比较,若小于较大的子节点,则交换,以保持大顶堆

        直到该元素不再小于其子节点或到达堆的底部。

        该过程称为下移: shift down

        

class Maxheap:
    max_heap = []

    def pop(self) -> int:
        # 堆为空
        if not self.max_heap:
            raise IndexError("堆为空")

        size = len(self.max_heap)
        self.max_heap[0], self.max_heap[size - 1] = self.max_heap[size - 1], self.max_heap[0]
        # 删除堆顶元素
        val = self.max_heap.pop()
        size -= 1

        self.__shift_down(0, size)
        # 拿到移除的元素
        return val


    def __shift_down(self, i: int, n: int):
        # 有子节点
        while 2 * i + 1 < n:
            left, right = 2 * i + 1, 2 * i + 2
            
            # 找到较大子节点的编号
            if right >= n:
                # 只有左子节点
                larger = left
            else:
                if self.max_heap[left] > self.max_heap[right]:
                    larger = left
                else:
                    larger = right
            # 当前父节点小于子节点,交换,此时交换后的节点继续与子节点比较
            if self.max_heap[i] < self.max_heap[larger]:
                self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i]
                i = larger
            # 当前父节点大于子节点,结束
            else:
                break

        时间复杂度为O(log⁡n),其中 n 是堆中元素的数量,因为堆的高度是 logn

7.4 堆排序

        算法思想:使用堆中的排序方法,建立大顶堆。重复地从堆中取出堆顶元素,并让剩余的堆结构继续维持大顶堆的性质。

        步骤:

        定义一个数组实现的堆结构,将原始数组的元素存入堆结构;

        从最后一个元素的父节点((size - 2)// 2  )开始,自下而上的,依次通过下移调整,构造大顶堆;

        交换堆顶元素与最后一个元素,完成后,使堆的长度减一;

        从堆顶元素开始,进行下移调整,维持堆的特性,直到堆的大小为1。

代码实现:

class Maxheap:
    ......
     def __buildMaxHeap(self, nums: [int]):      # 建堆
        size = len(nums)

        for i in range(size):
            self.max_heap.append(nums[i])

        # 自下向上对每个非叶子节点向下调整,建立大顶堆
        for i in range((size - 2) // 2, -1, -1):
            self.__shift_down(i, size)

    def maxHeapSort(self, nums: [int]) -> [int]:
        self.__buildMaxHeap(nums)

        size = len(nums)
        # 弹出堆顶元素,并将堆的规模减一
        for i in range(size - 1, -1, -1):
            self.max_heap[0], self.max_heap[i] = self.max_heap[i], self.max_heap[0]
            # 对新的堆顶元素向下调整
            self.__shift_down(0, i)

        return self.max_heap

class Solution:

   def maxHeapSort(self, nums: [int]) -> [int]:
        return Maxheap().maxHeapSort(nums)

    def sortArray(self, nums: [int]) -> [int]:
        return self.maxHeapSort(nums)

        时间复杂度为 O(n * logn)

        空间复杂度为 O(1)

        下移调整时,可能改变相同元素的相对位置,因此是 不稳定排序

08. 计数排序(Counting Sort)

        算法思想:通过统计数组中每个元素出现的次数,找到每个元素在数组中对应的位置。

        步骤:

        计算排序范围,找到最大元素 numsmax 与最小元素 numsmin;

        定义计数数组 counts ,用于统计每个元素出现的次数;

                counts 的索引值为 num - numsmin ,表示元素 num 在nums中的相对位置

        生成累积计数数组,从 counts 中的第一个元素开始,每一项累加前一项和,如(设0最小)

                counts[0] = 1        表示 0 出现了 1 次;

                counts[1] = 2        表示 1 出现了 2 次;

                counts[2] = 2        表示 2 出现了 2 次;

                累积计数数组中

                counts[1] += counts[0]        此时 counts[1] = 3,表示位置 1 到 3 是元素 0 和 1 的位置

                同理 counts[2] += counts[1]       此时 counts[2] = 5 ,表示位置 1 到 5 是元素 0、1、2;

        逆序填充目标数组,逆序遍历数组 nums,将每个元素 num 填入正确位置;

        将其填充到结果数组 res 中,位置为 counts[num - numsmin];

        放入后,另累计数组中对应索引位置 减一,从而得到下一个元素的放置位置;

                例如

                        counts[2] = 5

                        放置了一个元素 2 到位置 5,若放置第 2 个元素 2,要将其放置到位置 4 

                        因此在放置一个元素 2 后 counts[2] -= 1,用于记录下一个元素 2 的放置位置

      

代码实现:

    def countingSort(self, nums: [int]) -> [int]:
        numsMax, numsMin = max(nums), min(nums)
        size = numsMax - numsMin + 1
        counts = [0 for _ in range(size)]
        # 计数
        for i in range(len(nums)):
            counts[nums[i] - numsMin] += 1
        # 累积数组
        for i in range(1, size):
            counts[i] += counts[i - 1]
        # 将元素倒序归位
        res = [0 for _ in range(len(nums))]
        for i in range(len(nums) - 1, -1, -1):
            num = nums[i]

            res[counts[num - numsMin] - 1] = num
            counts[num - numsMin] -= 1
        return res

        时间复杂度为 O(n + k),其中 k 为待排序数组的值域;

        空间复杂度为 O(k)。

09. 桶排序(Bucket Sort)

        算法思想:将待排序数组中的元素分散到若干个【桶】中,然后对每个桶中的元素再进行单独排序。

        步骤:

        确定桶的数量,根据待排序数组的值域范围,划分为 k 个桶;

        分配元素,将每个元素根据大小分到对应的桶中;

        对每个桶进行排序;

        合并桶内元素。

代码实现:

    def insertionSort(self, nums: [int]) -> [int]:      # 定义桶内的排序算法
        for i in range(1, len(nums)):
            temp = nums[i]
            j = i
            while j > 0 and nums[j - 1] > temp:
                nums[j] = nums[j - 1]
                j -= 1
            nums[j] = temp
        return nums

    def bucketSort(self, nums: [int], bucketSize = 5) -> [int]:
        numsMax, numsMin = max(nums), min(nums)
        buckectCount = (numsMax - numsMin) // bucketSize + 1
        buckets = [[] for _ in range(buckectCount)]

        # 遍历数组,将元素根据大小放入桶中
        for num in nums:
            buckets[(num - numsMin) // bucketSize].append(num)

        # 对每个桶内的元素单独排序,并添加到res数组中
        res = []
        for buckest in buckets:
            self.insertionSort(buckest)
            res.extend(buckest)

        return res

        时间复杂度 O(n),当输入元素个数为 n,桶的个数是 m 时,每个桶里的数据就是 k=n / m​ 个。每个桶内排序的时间复杂度为 O(k×log⁡k)。m 个桶就是

                                 m×O(k×log⁡k)=m×O(n/m×log⁡n/m)=O(n×log⁡n/m)

当桶的个数 m 接近于数据个数 n 时,log⁡n/m​ 就是一个较小的常数,所以排序桶排序时间复杂度接近于 O(n)

        空间复杂度 O(n + m);

        排序稳定性取决与桶内部的排序算法。

10. 基数排序(Radix Sort)

        算法思想:将整数按位数切割成不同的数字,然后从低位开始,依次到高位,逐位进行排序

        步骤:

        确定排序最大值的位数;

        从最低位到最高位逐一对每一位排序;

                定义一个长度为 10 的桶数组 buckects ,每个桶分别代表 0~9 这十个数字;

                按照每个元素当前位上的数字,将元素放到对应桶中;

                清空原始数组,然后按照桶的顺序,依次取出对应元素,加入原始数组。

代码实现:

    def radixSort(self, nums: [int]) -> [int]:
        # 桶的大小位所有元素的最大位数
        size = len(str(max(nums)))

        # 从低位开始,遍历每一位
        for i in range(size):
            buckets = [[] for _ in range(10)]
            # 按照当前位上的数字,放入桶中
            for num in nums:
                buckets[num // (10 ** i) % 10].append(num)

            nums.clear()

            for bucket in buckets:
                for num in bucket:
                    nums.append(num)

        return nums

        时间复杂度 O(n * k);

        空间复杂度 O(n + k),其中 k 是数组位数。

排序例题

1. LCR 164. 破解闯关密码 - 力扣(LeetCode) 

2. 283. 移动零 - 力扣(LeetCode)

        第二题中,可以参考快速排序的思路,将不等于 0 的元素放在 0 左边,等于 0 放在右边。

 

             PS:图是从题解里拿来的(侵删) 

3. 215. 数组中的第K个最大元素 - 力扣(LeetCode)

        第三题中,堆排序可以找到第 k 大的元素, 但是时间复杂度还不符合要求,可以考虑使用桶排序,根据桶的大小,对某个桶内的子数组排序,以降低时间复杂度。

4. 75. 颜色分类 - 力扣(LeetCode)

5. 88. 合并两个有序数组 - 力扣(LeetCode)

        第五题可以类比归并排序中的 merge 过程,我实现的是从小到达的插入

class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        """
        Do not return anything, modify nums1 in-place instead.
        """
        # 归并排序的 merge 过程

        if m == 0:
            nums1.clear()
            for num in nums2:
                nums1.append(num)
            return
        if n == 0:
            return

        cnt = len(nums1) - m
        for _ in range(cnt):
            nums1.pop()
        i, j = 0, 0
        while i < m and j < n:
            if nums2[j] <= nums1[i]:
                nums1.insert(i, nums2[j])
                j += 1
                i += 1
                m += 1
            else:
                i += 1
            
        while j < n:
            nums1.append(nums2[j])
            j += 1
        

        操纵双指针,实现正确位置的插入过程。

        更好的思路是从后向前遍历,题目给出了最后元素的位置,最大元素放在最后位置即可。

6. 506. 相对名次 - 力扣(LeetCode)

        第六题是一道比较有趣的排序题,可以使用 zip() 方法绑定排序后的名次与分数,也可以像我一样使用下标手动绑定解决

class Solution:
    def findRelativeRanks(self, score: List[int]) -> List[str]:
        aid = score.copy()
        aid.sort(reverse = True)
        ans = []
        for i in range(len(score)):
            rank = aid.index(score[i]) + 1
            if rank == 1:
                ans.append("Gold Medal")
            elif rank == 2:
                ans.append("Silver Medal")
            elif rank == 3:
                ans.append("Bronze Medal")
            else:
                ans.append(str(rank))
        return ans

7. LCR 170. 交易逆序对的总数 - 力扣(LeetCode)

        第七题是归并排序的经典应用场景,在 merge 的过程中,对逆序对计数。

class Solution:
    def reversePairs(self, record: List[int]) -> int:
        def mergeSort(low, high):
            if low >= high:
                return 0
            mid = (low + high) // 2
            ans = mergeSort(low, mid) + mergeSort(mid + 1, high)
            # 双指针记录左右两侧的合并位置
            i, j = low, mid + 1
            tmp[low: high + 1] = record[low: high + 1]
            for k in range(low, high + 1):
                # 如果左侧为空,则将右侧数据加入
                if i == mid + 1:
                    record[k] = tmp[j]
                    j += 1

                # 如果右侧为空,将左侧数据加入
                # 如果右侧大于等于左侧,则没有逆序对,将左侧数据加入
                elif j == high + 1 or tmp[j] >= tmp[i]:
                    record[k] = tmp[i]
                    i += 1
                
                # 如果右侧小于左侧,则当前右侧元素与左侧剩余的 m 个元素构成了 m 个逆序对
                # 左侧剩余元素个数为 mid - i + 1
                else:
                    record[k] = tmp[j]
                    j += 1
                    ans += mid - i + 1
            return ans

        tmp = [0] * len(record)
        return mergeSort(0, len(record) - 1)

总结

        本盘介绍了十种排序算法,学习的过程中可能有疑问,Python中的 sort() 函数是快速排序算法(Quick Sort)实现的,使用时直接调用即可,为什么还要学习这么排序算法。

        其实在做题的过程中可以明白,排序的过程是更重要的,例题中过次用到了归并(merge)的思想,这是分治法的一种生动体现,使用了堆排序找到第 k 大元素,也使用了快速排序中,双指针的思路。

        因此思路比实现的效果更重要。

算法通关手册(LeetCode) | 算法通关手册(LeetCode)

原文内容在这里,如有侵权,请联系我删除。

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值