数据结构与算法 — 排序问题(LeetCode 912)

一、插入排序(稳定排序)

        基本思想:将一个元素插入到已经排好序的有序表中,从而得到一个新的、元素数增1的有序表。

        实现思路:使用双层循环。外层循环是对除了第1个元素之外的所有元素,内层循环是对当前元素前面的有序表进行待插入位置查找,并进行移动。

        时间复杂度:O(n^2),这里 n 是元素的总个数。当是对「有序表」做排序时,为最优时间复杂度O(n);当是对「逆序表」做排序时,为最坏时间复杂度O(n^2)。

        空间复杂度:O(1)。

class Solution(object):
    def sortArray(self, nums):
        len_nums = len(nums);
        if len_nums==0:
            retrun [];
        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 = j-1;
            nums[j] = temp;     # j记录的是:当前元素nums[i](也即temp)的待插入位置
        return nums;

        插入排序的优点:在「几乎有序」的数组上表现良好;

                在「短数组」上的表现也很好。因为「短数组」的特点是:每个元素离它最终排定的位置都不会太远。因此,在「小区间」内执行排序任务的时候,可以使用

           「插入排序」。

 

二、归并排序(稳定排序)

        基本思想:借助额外的空间,将已有序的子序列合并,从而得到完全有序的序列。即,先使每个子序列有序,再使子序列段间有序。

        实现思路:递归实现,

        1. 把长度为 n 的输入序列,分成长度 n/2 的子序列;

        2. 对两个子序列分别使用「归并排序」;

        3. 合并所有子序列。

        时间复杂度:O(n*log(n)),这里 n 是元素的总个数。该时间复杂度是由数学归纳法推导得到的「T(n) = 2*T(n/2) + 合并时间 = 2*T(n/2) + n = … = n*T(1) + log(n)*n,T(1) = 0」。

        空间复杂度:?。

class Solution(object):
   # 归并排序: 写法1
    def sortArray(self, nums):
        len_nums = len(nums);
        if len_nums<=1:
            return nums;
        mid = len_nums/2;
        left = self.sortArray(nums[:mid]);
        right = self.sortArray(nums[mid:]);
        return self.Merge_sortedArray(left, right);

    def Merge_sortedArray(self, 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 = i+1;
            else:
                res.append(right[j]);
                j = j+1;
        res.extend(left[i:]);     # 如果用的是.append()就报错;因为当left[i:]为[]时,append方法也会将[]添加到res中。本语句还可写为: res += left[i:]
        res.extend(right[j:]);    # 如果用的是.append()就报错;因为当right[j:]为[]时,append方法也会将[]添加到res中。本语句还可写为: res += right[j:]
        return res;

        归并排序的优化实现:

        优化 ①:在分割得到的「小区间」子序列上转而使用「插入排序」,Java 源码里面也有类似这种操作。「小区间」的长度是个超参数,需要测试决定,比如 JDK 的timsort源码中设为7。

        优化 ②:全程使用一个临时数组tmp进行「合并两个有序数组」的操作,避免多次创建临时数组和销毁的消耗。

class Solution(object):
    # 归并排序: 写法2
    def sortArray(self, nums):
        n = len(nums)
        if n<=1:
            return nums
        tmp = [0] * n
        return self.mergeSort(nums, tmp, 0, n - 1)
    def mergeSort(self, nums, tmp, l, r):
        if l >= r:
            return ;
        mid = (l + r) // 2
        self.mergeSort(nums, tmp, l, mid)
        self.mergeSort(nums, tmp, mid + 1, r)
        i, j, pos = l, mid + 1, l
        while i <= mid and j <= r:
            if nums[i] <= nums[j]:
                tmp[pos] = nums[i]
                i += 1
            else:
                tmp[pos] = nums[j]
                j += 1
            pos += 1
        for k in range(i, mid + 1):
            tmp[pos] = nums[k]
            pos += 1
        for k in range(j, r + 1):
            tmp[pos] = nums[k]
            pos += 1
        nums[l:r+1] = tmp[l:r+1];
        return nums;

        时间复杂度:O(n*log(n)),这里 n 是元素的总个数。该时间复杂度是由数学归纳法推导得到的「T(n) = 2*T(n/2) + 合并时间 = 2*T(n/2) + n = … = n*T(1) + log(n)*n,T(1) = 0」。

        空间复杂度:O(n)。

 

三、快速排序(不稳定排序)

        基本思想:每一次都只排定一个元素的位置(这个元素会呆在它最终应该呆的位置);然后递归地去排它左边的部分和右边的部分,直到数组有序。

        实现思路:递归 + 双指针实现,

        1. 选定一个基准元素pivot(通常选数组的第1个元素);

        2. 利用元素pivot将数组分为2部分,pivot左边的元素均 <= pivot,pivot右边的元素均 > pivot;

        3. 对被元素pivot分出的左、右2部分元素,分别使用「快速排序」。

        时间复杂度:O(n*log(n)),这里 n 是元素的总个数。最坏情况下,是O(n^2),即当数组是已排好序的数组时。

        空间复杂度:O(log(n)),这里的空间占用主要来自递归函数占用的栈空间。最坏情况下,是O(n),即当数组是已排好序的数组时。

        递归函数占用的栈空间是指:当前这轮的递归函数还没有进行完,就先去执行下一个递归函数,那么这个还没进行完的递归函数就需要在栈中保存,方便下一个递归函数调用结

        束时,能继续执行这个递归函数。

class Solution(object):
    def sortArray(self,nums):
        len_nums = len(nums);
        return self.quick(nums, 0, len_nums-1);

    def quick(self, nums, left, right):
        if left >= right:
            return nums;
        pivot = left;
        i = left;
        j = right;
        while i < j:
            while i < j and nums[j] > nums[pivot]:     # 这里的2个while循环的顺序不能颠倒;如果颠倒,那么当nums[left]-nums[right]在未排序前就已经有序时,
                j = j-1;                               # i往右多走了一步,j往左少走了一步,会把这段有序序列打乱。
            while i < j and nums[i] <= nums[pivot]:
                i = i+1;
            nums[i], nums[j] = nums[j], nums[i];
        nums[pivot], nums[j] = nums[j], nums[pivot];   # 这里写nums[j]或nums[i]都可
        self.quick(nums, left, j - 1);                 # 这里写j-1或i-1都可
        self.quick(nums, j + 1, right);                # 这里写j+1或i+1都可
        return nums; 

 

 四、堆排序(不稳定排序)

        基本思想:把未排定的部分构建成一个「堆」,这样就能以 O(log(N)) 的方式选出最大元素

        实现思路:利用循环 + 「堆」的节点性质实现,

        1. 对「堆」中第1个非叶子节点开始,从右向左、从下向上地对每个非叶子节点做下沉操作,构建出「最大堆」;

        2. 循环实现以下步骤:

                               1) 把堆顶元素(当前最大)交换到数组末尾;

                               2) 对新换上来的堆顶元素做下沉操作;

                               直到堆中无元素可与堆顶元素交换、下沉。

        时间复杂度:O(n*log(n)),这里 n 是元素的总个数。具体来说,初始化堆的时间复杂度为O(n),调整堆的时间复杂度为O( log(n!) ) ≈ n*log(n)。

        空间复杂度:O(1)

class Solution(object):
    def sortArray(self,nums):
        self.Build_Heap(nums);
        len_nums = len(nums);
        i = len_nums-1;
        while i>=1:
            nums[0],nums[i] = nums[i], nums[0];
            i = i-1;
            self.siftdown(nums, 0, i);
        return nums;
    
    # 构建堆
    def Build_Heap(self, nums):
        len_nums = len(nums);
        for i in reversed(range(len_nums//2)):
            self.siftdown(nums, i, len_nums-1);

    # 对数组下标为k的元素,做下沉操作(迭代写法)
    def siftdown(self, nums, k, end):
        while (2*k+1)<=end:
            left = 2*k+1;
            right = left+1;
            large = left;
            if right<=end and nums[left]<nums[right]:
                large = right;
            if nums[k]<nums[large]:
                nums[k], nums[large] = nums[large], nums[k];
            else:
                break;
            k = large; 

    # 对数组下标为k的元素,做下沉操作(递归写法)
    def siftdown(self, nums, k, end):
        left = 2*k+1;
        right = left + 1;
        large = left;
        if left <= end:
            if right <= end and nums[left]<nums[right]:
                large = right;
            if nums[large] > nums[k]:
                nums[k], nums[large] = nums[large], nums[k];
                self.siftdown(nums, large, end);     

         堆的节点性质:

         1)从下往上、从右往左数,堆中第一个非叶子节点的数组下标为:n//2,这里n是元素的总个数。

         2)假设堆是一棵满二叉树,则这棵树共有H = log2(n+1) 层,共有 H-1 层非叶子节点,其中第 i 层共有2^(i-1)个节点。第 i 层的每个节点做下沉操作时,最多需要的操作次数为 H - i 次。【分析初始化(即创建)堆的时间复杂度时,可用】

     3)如何计算调整堆的时间复杂度:假设根节点和排在最后的序号为m的叶子结点交换、并进行调整,那么调整的操作次数 = 原来m结点所在的层数 = log(m+1),这里 m 的取值范围从1 ~ n-1。

           

posted @ 2020-08-01 15:48  花与  阅读(296)  评论(0)    收藏  举报