算法锦囊7:一文搞定排序算法

239 阅读4分钟

这是我参与更文挑战的第13天,活动详情查看: 更文挑战

最近想把自己刷算法题的经验心得整理一下,一方面为了复习巩固,另一方面也希望我的分享能够帮助到更多在学习算法的朋友。

专栏名称叫《算法锦囊》,在讲解算法时会注重整体性,但不会面面俱到,适合有一定算法经验的人阅读。

这一次我们重点来看排序算法,这一部分的所有题目和源码都上传到了github的该目录下,题解主要用Python语言实现。

概述

我们在学习数据结构和算法时,最开始接触的就是排序算法。回想那时候,可能连一个现在看起来很简单的冒泡排序,当时能写出来也会欣喜若狂。

今天我们来回归初心,通过leetcode上的 912. 排序数组,来温习各种排序算法。

冒泡排序

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

class Solution:
    def bubble_sort(self, nums: List[int]) -> List[int]:
        for i in range(len(nums)-1):
            for j in range(len(nums)-i-1):
                if nums[j] > nums[j+1]:
                    nums[j], nums[j+1] = nums[j+1], nums[j]
        return nums

在传统的冒泡排序的基础上,可以进一步优化,只要某一次遍历时未发生元素的交换,即可认为当前数组已经是顺序的了,可直接退出循环。

class Solution:
    def bubble_sort(self, nums: List[int]) -> List[int]:
        swapped = True
        for i in range(len(nums)-1):
            if not swapped:
                break
            swapped = False
            for j in range(len(nums)-i-1):
                if nums[j] > nums[j+1]:
                    nums[j], nums[j+1] = nums[j+1], nums[j]
                    swapped = True
        return nums

插入排序

插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

class Solution:
    def insert_sort(self, nums: List[int]) -> List[int]:
        for i in range(1, len(nums)):
            if nums[i] < nums[i-1]:
                temp = nums[i]
                index = i
                for j in range(i-1, -1, -1):
                    if nums[j] > temp:
                        nums[j+1] = nums[j]
                        index = j
                    else:
                        break
                nums[index] = temp
        return nums

选择排序

选择排序的原理是,首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

class Solution:
    def select_sort(self, nums: List[int]) -> List[int]:
        # i 的值对应于已排序值的数量
        for i in range(len(nums)):
            # 我们假设未排序部分的第一项是最小的
            lowest_value_index = i
            # 这个循环用来迭代未排序的项
            for j in range(i + 1, len(nums)):
                if nums[j] < nums[lowest_value_index]:
                    lowest_value_index = j
            # 将未排序元素的最小的值与第一个未排序的元素的值相交换
            nums[i], nums[lowest_value_index] = nums[lowest_value_index], nums[i]
        return nums

归并排序

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

class Solution:
    def merge_sort(self, nums: List[int]) -> List[int]:
        def merge(left, right):
            res = []
            i = 0
            j = 0
            while i < len(left) and j < len(right):
                if left[i] <= right[j]:
                    res.append(left[i])
                    i += 1
                else:
                    res.append(right[j])
                    j += 1
            res += left[i:]
            res += right[j:]
            return res
        if len(nums) <= 1:
            return nums
        mid = len(nums) // 2
        # 分
        left = self.merge_sort(nums[:mid])
        right = self.merge_sort(nums[mid:])
        # 合并
        return merge(left, right)

快速排序

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

class Solution:
    def quick_sort(self, nums):
        n = len(nums)

        def quick(left, right):
            if left >= right:
                return nums
            pivot = left
            i = left
            j = right
            while i < j:
                while i < j and nums[j] > nums[pivot]:
                    j -= 1
                while i < j and nums[i] <= nums[pivot]:
                    i += 1
                nums[i], nums[j] = nums[j], nums[i]
            nums[pivot], nums[j] = nums[j], nums[pivot]
            quick(left, j - 1)
            quick(j + 1, right)
            return nums

        return quick(0, n - 1)

上面这种写法,因为哨兵每次固定放在左边,特殊情况下,容易使快速排序时间复杂度退化成O(n^2)

改进版为随机选取。

class Solution:
    def quick_sort(self, nums):
        n = len(nums)

        def quick(left, right):
            if left >= right:
                return
            pivot = random.randint(left, right)
            i, j = left, right
            nums[left], nums[pivot] = nums[pivot], nums[left]

            while i < j:
                while i < j and nums[j] > nums[left]:
                    j -= 1
                while i < j and nums[i] <= nums[left]:
                    i += 1
                nums[i], nums[j] = nums[j], nums[i]
            nums[left], nums[j] = nums[j], nums[left]
            quick(left, j - 1)
            quick(j + 1, right)
            return

        quick(0, n - 1)
        return nums

对比归并排序和快速排序。归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题。归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法。归并之所以是非原地排序算法,主要原因是合并函数无法在原地执行。快速排序通过设计巧妙的原地分区函数,可以实现原地排序,解决了归并排序占用太多内存的问题。

堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

class Solution:
    def heapify(self, nums, heap_size, root_index):
        # 设最大元素索引为根节点索引
        largest = root_index
        left_child = (2 * root_index) + 1
        right_child = (2 * root_index) + 2

        # 如果根节点的左子节点是有效索引,并且元素大于当前最大元素,则更新最大元素
        if left_child < heap_size and nums[left_child] > nums[largest]:
            largest = left_child

        # 对根节点的右子节点执行相同的操作
        if right_child < heap_size and nums[right_child] > nums[largest]:
            largest = right_child

        # 如果最大的元素不再是根元素,则交换它们
        if largest != root_index:
            nums[root_index], nums[largest] = nums[largest], nums[root_index]
            # 调整堆以确保新的根节点元素是最大元素
            self.heapify(nums, heap_size, largest)

    def heap_sort(self, nums):
        n = len(nums)

        # 利用列表创建一个最大堆
        # range 的第二个参数表示我们将停在索引值为 -1 的元素之前,即列表中的第一个元素
        # range 的第三个参数表示我们朝反方向迭代
        # 将 i 的值减少1
        for i in range(n, -1, -1):
            self.heapify(nums, n, i)

        # 将最大堆的根元素移动到列表末尾
        for i in range(n - 1, 0, -1):
            nums[i], nums[0] = nums[0], nums[i]
            self.heapify(nums, i, 0)
        
        return nums

其他排序

因篇幅限制,其他一些冷门的排序简单介绍一下,不做代码演示了。

计数排序(Counting Sort)

计数排序要求输入的数据必须是有确定范围的整数。将输入的数据值转化为键存储在额外开辟的数组空间中;然后依次把计数大于 1 的填充回原数组

桶排序(Bucket Sort)

桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

基数排序(Radix Sort)

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。

总结

最后,整理一下各个排序算法的时间复杂度空间复杂度以及稳定性情况。

image.png

参考资料

《数据结构和算法之美》

《十大经典排序算法(动画演示)》