Loading

[数据结构]排序算法

冒泡排序
直接插入排序
选择排序
快速排序
归并排序
希尔排序
堆排序
桶排序

时间复杂度:快希以nlog2n的速度归堆
稳定性:考研情绪不稳定,快些选一堆好友

归并排序

原理

归并排序的原理是这样的,比如一开始我们有数组[9,6,3,2,5,8,7,4,1,0]

然后我们开始平分并递归

[9,6,3,2,5,8,7,4,1,0]
[9,6,3,2,5]  [8,7,4,1,0]
[9,6,3]  [2,5]  [8,7,4]  [1,0]
[9,6]  [3]  [2]  [5]   [8,7]  [4]   [1]  [0]
[9]  [6]  [3]  [2]  [5]  [8]  [7]  [4]  [1]  [0]

----递归返回----

[6,9]  [2,3]  [5,8]  [4,7]  [0,1]
[2,3,6,9]  [4,5,7,8]  [0,1]
[2,3,4,5,6,7,8,9]  [0,1]
[1,2,3,4,5,6,7,8,9]

时间复杂度分析

由于我们每次都是平分区间,所以可以看成一棵二叉树,树的高度就是log2^n,
(这里出现了第一个问题,如果我们不是每次平分树的高度就会变高,例如每次都是左边分一个,右边分剩下的,树的高度就会变成n)

等到每个区间分到只剩下一个元素,我们开始递归返回,
在最下面一层,我们要把所有元素两两合并,这个操作要执行n/2次,要对n个元素都判断大小,所以这一层的时间复杂度是O(n)
在倒数第二层,我们要把所有元素两两合并,这个操作要执行n/4次,要对n个元素都判断大小,所以这一层的时间复杂度是O(n)
...
在第二层,我们要把所有元素两两合并,这个操作要执行2次,要对n个元素都判断大小,所以这一层的时间复杂度是O(n)
在第一层,我们要把所有元素两两合并,这个操作要执行1次,要对n个元素都判断大小,所以这一层的时间复杂度是O(n)

综上这个递归树log2^n层,每层复杂度O(n)
总的复杂度,O(nlog2^n)

空间复杂度分析

空间复杂度不能像时间复杂度一样累加,因为当一层递归返回的时候,它占用的内存就会被释放,所以我们来看最大的内存占用其实是最后一次合并操作的时候,我们需要一个长度为n的数组一下把所有的元素都装进去,所以空间复杂度是O(n)

这个递归的栈空间消耗也要考虑,栈空间的消耗等于树的高度,因为递归的深度最高就是树的深度,所以是O(nlog2^n)

实现

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:

        def merge(nums1, nums2):
            if not nums1 and not nums2:
                return None
            if not nums1:
                return nums2
            if not nums2:
                return nums1

            len1 = len(nums1)
            len2 = len(nums2)

            index1 = 0
            index2 = 0

            res = []
            while index1 <= len1 - 1 and index2 <= len2 - 1:
                if nums1[index1] <= nums2[index2]:
                    res.append(nums1[index1])
                    index1 += 1
                else:
                    res.append(nums2[index2])
                    index2 += 1

            while index1 <= len1 - 1:
                res.append(nums1[index1])
                index1 += 1
            while index2 <= len2 - 1:
                res.append(nums2[index2])
                index2 += 1

            return res

        def mergesort(nums):
            length = len(nums)
            if length == 0:
                return None
            if length == 1:
                return nums
            if length > 1:
                return merge(
                    mergesort(nums[: length // 2]), mergesort(nums[length // 2 :])
                )

        return mergesort(nums)

快速排序

原理

现在我们又有一个数组[7,5,3,8,9,6,2,4,1,0]

第一次我们随机选取一个数字 3 作为枢纽元素分割数组
[2,1,0] [3] [7,5,8,9,6,4]
对两侧数组同样进行分割,
[0] [1] [2] [3] [4] [5] [7,8,9,6]
长度为一直接返回,超过一的继续分割
[0] [1] [2] [3] [4] [5] [7] [8,9,6]
[0] [1] [2] [3] [4] [5] [7] [6] [8] [9]
---递归返回---
[0,1,2] [3,4] [5,6] [7,8,9]
[0,1,2,3,4] [5,6,7,8]
[0,1,2,3,4,5,6,7,8,9]

快速排序是先排序再划分,二叉树返回的时候直接无脑合并
归并排序是先划分,二叉树在返回的时候排序

时间复杂度分析

很显然这个树的高度受到每次是否平均划分的严重影响,在归并排序中我们强制每次平分,但是在快速排序中我们不能保证选出来的枢轴元素正好是二分点,为了尽可能的二分,我们会随机选取枢轴元素,避免在原始数组本来就是递增或者递减的情况下,我们每次选出来的枢轴都在一端的尴尬情况。

最好情况下,每次都二分,树的高度是log2^n,每一层的复杂度都是O(n),因为想要划分数组需要比较n次,复杂度是O(nlogn)
最差情况下,每次都分成1和剩余其他,树的高度为n,每一层的复杂度都是O(n),因为想要划分数组需要比较n次,复杂度是O(n^2)

空间复杂度分析

快速排序是原地排序,这样看空间复杂度是O(1)
但是这个递归的栈空间消耗也要考虑,栈空间的消耗等于树的高度,因为递归的深度最高就是树的深度,所以在最好情况下是O(nlog2^n)最坏情况下是O(n)

实现

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:

        def quciksort(nums):
            if not nums or len(nums) == 1:
                return nums
            pivot = random.choice(nums)

            less = []
            equal = []
            bigger = []

            for n in nums:
                if n == pivot:
                    equal.append(n)
                if n > pivot:
                    bigger.append(n)
                if n < pivot:
                    less.append(n)
            return quciksort(less) + equal + quciksort(bigger)

        return quciksort(nums)

堆排序

原理

建堆:首先把元素摆放成一棵完全二叉树
调整堆:从最后一个非叶子结点开始向上调整,这个复杂度是O(n)
堆排序:取下根节点,把最后一个叶子结点替换上去,调整这个根节点,这个复杂度是O(nlog2n)

时间复杂度

o(nlog2n)

空间复杂度

posted @ 2024-07-16 17:24  Duancf  阅读(17)  评论(0)    收藏  举报