剑指offer(python)-题目30-最小的k个数--7种排序方法*******

题目描述
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

题目分析:

思路1: 可以先对这几个数进行排序(sort函数,O( n*log2(n) )),切片选择前k 个

# -*- coding:utf-8 -*-
class Solution:
    def GetLeastNumbers_Solution(self, tinput, k):
        if tinput == [] or k > len(tinput):
            return []
        tinput.sort()
        return tinput[: k]

思路2:冒泡排序
冒泡排序可以看成是一列竖着的气泡,关键字字轻的往上浮,两个数字之间进行比较,先要 拿第一个和第二个对比 如果第一个比第二个大那么就换位置,如果小就不换,接下来拿第二个和第三个…这样依次下去比到最后 我们把这些数中最大的一个放在了最右下边(即最小的在左端), 然后再重复以上的方式下去就这些数只第二个大的放在了倒数第二位,依次

1、比较相邻的元素。如果第一个比第二个大(小),就交换他们两个。
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大(小)的数。
3、针对所有的元素重复以上的步骤,除了最后已经选出的元素(有序)。
4、持续每次对越来越少的元素(无序元素)重复上面的步骤,直到没有任何一对数字需要比较,则序列最终有序

在这里插入图片描述
**

时间复杂度: 最好情况是 一次遍历即可完成排序,即正序情况, 比较次数是=n-1, 移动次数是0, o(n)=n-1
最坏情况是反序,比较次数是n+n-1+n-2+…1=n*(n-1)/2
平均时间复杂度是O(n^2),空间复杂度是O(1)

**

那么接下来,我们来分析一下,如何用程序实现这个逻辑:

1、我们就定5个数吧,那么我们可以把这5个数放在一个数组里。list={10,1,35,61,89,}

2、要进行4轮的比较才能确定,这一定是个循环,固定次数的,我们用for吧。for i in range(0,4) ,数组是从0开始的,所以习惯i从0开始,这样也有利于看直接:数组[i], 4也就是:数组.length-1

3、每一轮里面要循环不同次数比较数据确定位置,那么在第2步循环中,要再有一个循环,这是多重循环,也叫潜逃循环

这个for循环,我们也要来分析一下,它的范围:

第几轮(我们定的是变量i) (内层循环)次 变量j

1 对应数组下标i是0          4 外层循环第1次,内层需要循环4次 =5-i-1
 2 对应数组下标i是1          3 外层循环第2次,内层需要循环3次 =5-i-1
  3 对应数组下标i是2         2 外层循环第3次,内层需要循环2次 =5-i-1
  4 对应数组下标i是3          1 外层循环第4次,内层需要循环1次 =5-i-1

双重循环的原理是:外层循环1次,内层循环1轮(遍历),上表已经很清晰的标示出了j每轮循环里面要执行的次数,但因为我们是i是从0开始的,所以j的范围应该是5-i-1,也就是:数组.length-1-i

4、在内层循环中还有一个条件,即前一位比后一位大的话,进行挪位,不大则不变位置,如果涉及挪位的时候,我们需要一个变量来倒腾一下交换要挪位的2个值。

链接:https://www.nowcoder.com/questionTerminal/6a296eb82cf844ca8539b57c23e6e9bf
来源:牛客网

# -*- coding:utf-8 -*-
class Solution:
    def GetLeastNumbers_Solution(self, tinput, k):
        # write code here
        def bubble_sort(lst):
            if lst == []:
                return []
            for i in range(len(lst)):
                for j in range(1, len(lst) - i):
                    if lst[j-1] > lst[j]:
                        lst[j-1], lst[j] = lst[j], lst[j-1]  #交换
            return lst
 
        if tinput == [] or k > len(tinput):
            return []
        tinput = bubble_sort(tinput)
        return tinput[: k]

*思路3:快速排序,

即先在这个6 1 2 7 9 3 4 5 10 8序列中随便找一个数作为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来做啥的了)。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列。

方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即j=10),指向数字8。

首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
在这里插入图片描述
现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下。
6 1 2 5 9 3 4 7 10 8
在这里插入图片描述
到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下。
6 1 2 5 4 3 9 7 10 8
第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下。
3 1 2 5 4 6 9 7 10 8
在这里插入图片描述
到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。

在这里插入图片描述
OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧。
左边的序列是“3 1 2 5 4”。请将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于

等于3。好了开始动笔吧。

如果你模拟的没有错,调整完毕之后的序列的顺序应该是。
2 1 3 5 4

OK,现在3已经归位。接下来需要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕之后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“2 1”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下。
1 2 3 4 5 6 9 7 10 8

对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下。
1 2 3 4 5 6 7 8 9 10

时间复杂度分析: 快速排序是一次移动多个位置,
最好情况是,每一次划分后,划分的左侧节点和右侧长度一样,类似于构建一个二叉树,为O(Nlog2N)
最坏情况是每一次划分后只得到比上一次划分序列少一个,是n+n-1+…1=n*(n-1)/2 O(n^2)
平均时间复杂度为O(Nlog2N),空间复杂度也一样

快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。

-*- coding:utf-8 -*-
class Solution:
    def GetLeastNumbers_Solution(self, tinput, k):
        # write code here
        def quick_sort(lst):
            if not lst: # if lst is not None
                return []
            pivot = lst[0]  #取序列中第一个数为基数
            left = quick_sort([x for x in lst[1: ] if x < pivot]) #递归序列,先从右往左找一个小于基数的数,序列里全是小于基数的
            right = quick_sort([x for x in lst[1: ] if x >= pivot]) #再从左往右找一个大于等于基数的数,序列里全是大于基数的
            return left + [pivot] + right # 小于基数的 基数 大于基数的 基数此时 下标等于其自身
 
        if tinput == [] or k > len(tinput):
            return []
        tinput = quick_sort(tinput)
        return tinput[: k]

思路4:直接选择排序
思想是每次排序在当前序列中找到最小或者最大的,添加到有序序列中,比如待排序序列A{1,,,n}进行n-1遍处理,将最小者与A[1]交换,第二遍是将次小者与A[2]交换,依次类推。每一趟从待排序的数据元素中选出最小(最大)的元素,顺序放在待排序的数列最前,直到全部待排序的数据元素全部排完。

与冒泡排序的区别是:冒泡排序是每次相邻元素的比较,发现不对就立刻交换,直接选择排序是先检索一遍,找到最小的再进行交换。
在这里插入图片描述

最差时间复杂度:O(n^2)

 最优时间复杂度:O(n^2)

 平均时间复杂度:O(n^2)

 所需辅助空间:O(1)

实现方法:
双重循环,外层i控制当前序列最小(最大)值存放的数组元素位置,内层循环j控制从i+1到n序列中选择最小的元素所在位置。

# -*- coding:utf-8 -*-
class Solution:
    def GetLeastNumbers_Solution(self, tinput, k):
        # write code here
        def select_sort(lst):
            if lst == []:
                return []
            for i in range(len(lst)-1):  #先设定第一个元素是最小的元素,下标
                smallest = i
                for j in range(i, len(lst)): # 和后续的所有元素进行比较。如果比它还小,则交换位置
                    if lst[j] < lst[smallest]:
                        smallest = j
                lst[i], lst[smallest] = lst[smallest], lst[i]
 
            return lst
 
        if tinput == [] or k > len(tinput):
            return []
        tinput = select_sort(tinput)
        return tinput[: k]

思路5:堆排序

当数据很大时,做法就是使用一个堆来做,特别适合当数据很大的时候。这个其实相当于一个信息检索的过程。据说百度喜欢问此类问题。

heapq.nlargest(n, iterable, key=None) 返回最大的n个元素(Top-K问题)
heapq.nsmallest(n, iterable, key=None) 返回最小的n个元素(Top-K问题)

>    #-*- coding:utf-8 -*-
>     import heapq
>     class Solution:
>         def GetLeastNumbers_Solution(self, tinput, k):
>             if len(tinput) < k:
>                 return []
>             return heapq.nsmallest(k, tinput)
>     这个前提是有heapq 这个模块

堆的概念

在介绍堆排序之前,首先需要说明一下,堆是个什么玩意儿。

堆是一棵顺序存储的完全二叉树。

其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆。

其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆。

举例来说,对于n个元素的序列{R0, R1, … , Rn}当且仅当满足下列关系之一时,称之为堆:

(1) Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)

(2) Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)

其中i=1,2,…,n/2向下取整;

在这里插入图片描述

如上图所示,序列R{3, 8, 15, 31, 25}是一个典型的小根堆。

堆中有两个父结点,元素3和元素8。

元素3在数组中以R[0]表示,它的左孩子结点是R[1],右孩子结点是R[2]。

元素8在数组中以R[1]表示,它的左孩子结点是R[3],右孩子结点是R[4],它的父结点是R[0]。可以看出,它们满足以下规律:

设当前元素在数组中以R[i]表示,那么,

(1) 它的左孩子结点是:R[2*i+1];

(2) 它的右孩子结点是:R[2*i+2];

(3) 它的父结点是:R[(i-1)/2];

(4) R[i] <= R[2*i+1] 且 R[i] <= R[2i+2]。

基本思想:

首先,按堆的定义将数组R[0…n]调整为堆(这个过程称为创建初始堆),交换R[0]和R[n];

然后,将R[0…n-1]调整为堆,交换R[0]和R[n-1];

如此反复,直到交换了R[0]和R[1]为止。

以上思想可归纳为两个操作:

(1)根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大,大顶堆)。

(2)每次交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。

当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。

先通过详细的实例图来看一下,如何构建初始堆。

设有一个无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 }。


构造了初始堆后,我们来看一下完整的堆排序处理:

还是针对前面提到的无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 } 来加以说明。

在这里插入图片描述

时间复杂度 堆的存储表示是顺序的。因为堆所对应的二叉树为完全二叉树,而完全二叉树通常采用顺序存储方式。

**当想得到一个序列中第k个最小的元素之前的部分排序序列,最好采用堆排序。
**
因为堆排序的时间复杂度是O(n+klog2n),若k≤n/log2n,则可得到的时间复杂度为O(n)。 算法稳定性
堆排序是一种不稳定的排序方法。

因为在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,

因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的情况。

代码分析
这就是构建大根堆的思想,了解了之后就可以进行编码,编码主要解决两个问题:
如何把一个序列构造出一个大根堆
输出堆顶元素后,如何使剩下的元素构造出一个大根堆

这个需要再重新看看

class Solution:
    def GetLeastNumbers_Solution(self, tinput, k):
        # write code here
        def siftup(lst, temp, begin, end): #堆调整函数
            if lst == []:
                return []
            i, j = begin, begin * 2 + 1
            while j < end:
                if j + 1 < end and lst[j + 1] > lst[j]:
                    j += 1
                elif temp > lst[j]:
                    break
                else:
                    lst[i] = lst[j]
                    i, j = j, 2 * j + 1
            lst[i] = temp
 
        def heap_sort(lst):  #堆排序
            if lst == []:
                return []
            end = len(lst)
            for i in range((end // 2) - 1, -1, -1):
                siftup(lst, lst[i], i, end)
            for i in range(end - 1, 0, -1):
                temp = lst[i]
                lst[i] = lst[0]
                siftup(lst, temp, 0, i)
            return lst
 
        if tinput == [] or k > len(tinput):
            return []
        tinput = heap_sort(tinput)
        return tinput[: k]

**思路6:直接插入排序
直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。

在这里插入图片描述
时间复杂度
最好是正序 一次排完 O(n)
最合适O(n^2)
平均是O(n^2) 和冒泡排序一样,直接选择一样

# -*- coding:utf-8 -*-
class Solution:
    def GetLeastNumbers_Solution(self, tinput, k):
        # write code here
        def Insert_sort(lst):
            if lst == []:
                return []
            for i in range(1, len(lst)):
                temp = lst[i]
                j = i
                while j > 0 and temp < lst[j - 1]:
                    lst[j] = lst[j - 1]
                    j -= 1
                lst[j] = temp
            return lst
 
        if tinput == [] or k > len(tinput):
            return []
        tinput = Insert_sort(tinput)
        return tinput[: k]

希尔排序
希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
在这里插入图片描述
希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。目前还没有人给出选取最好的增量因子序列的方法。增量因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意:增量因子中除1 外没有公因子,且最后一个增量因子必须为1。希尔排序方法是一个不稳定的排序方法。

首先要明确一下增量的取法(这里图片是copy别人博客的,增量是奇数,我下面的编程用的是偶数):

  第一次增量的取法为: d=count/2;

  第二次增量的取法为:  d=(count/2)/2;

  最后一直到: d=1;

好,注意看图了,第一趟的增量d1=5, 将10个待排记录分为5个子序列,分别进行直接插入排序,结果为(13, 27, 49, 55, 04, 49, 38, 65, 97, 76)

第二趟的增量d2=3, 将10个待排记录分为3个子序列,分别进行直接插入排序,结果为(13, 04, 49, 38, 27, 49, 55, 65, 97, 76)

第三趟的增量d3=1, 对整个序列进行直接插入排序,最后结果为(04, 13, 27, 38, 49, 49, 55, 65, 76, 97)

重点来了。当增量减小到1时,此时序列已基本有序,希尔排序的最后一趟就是接近最好情况的直接插入排序。可将前面各趟的"宏观"调整看成是最后一趟的预处理,比只做一次直接插入排序效率更高

def shell_sort(arr):
    n = len(arr)
    gap = n//2
    while gap > 0:
        for i in range(gap, n):
            while i >= gap and arr[i] < arr[i - gap]: 
                arr[i], arr[i - gap] = arr[i - gap], arr[i]
                i -= gap  #这个是什么作用结束吗?
                #print(arr)
        gap //= 2
    return arr

思路7,归并排序**
即将两个或者两个以上的序列合并成一个序列
下面我们来看归并排序的思路(先讲思路再来具体讲归并的细节):
归并细节:
在这里插入图片描述
看成是 n 个有序的子序列(长度为 1),然后两两归并

得到 n/2 个长度为2 或 1 的有序子序列。继续两两归并
在这里插入图片描述
最后一趟
在这里插入图片描述

比如有两个已经排序好的数组,如何将他归并成一个数组?

我们可以开辟一个临时数组来辅助我们的归并。也就是说他比我们插入排序也好,选择排序也好多使用了存储的空间,也就是说他需要o(n)的额外空间来完成这个排序。只不过现在计算机中时间的效率要比空间的效率重要的多。无论是内存也好还是硬盘也好可以存储的数据越来越多,所以设计一个算法,时间复杂度是要优先考虑的。

整体来讲我们要使用三个索引来在数组内进行追踪
在这里插入图片描述
蓝色的箭头表示最终选择的位置,而红色的箭头表示两个数组当前要比较的元素,比如当前是2与1比较,1比2小,所以1放到蓝色的箭头中,蓝色的箭头后移,1的箭头后移。
在这里插入图片描述
然后2与4比较,2比4小那么2到蓝色的箭头中,蓝色箭头后移,2后移,继续比较…
在这里插入图片描述
归并思路就是这样了,最后唯一需要注意的是那个先比较完的话,那么剩下的直接不需要比较,把后面的直接移上去就可以了,这个需要提前判定一下。
时间复杂度是O(N log2N)


# -*- coding:utf-8 -*-
class Solution:
    def GetLeastNumbers_Solution(self, tinput, k):
        # write code here
        def merge_sort(lst):#分治
            if len(lst) <= 1:
                return lst
            mid = len(lst) // 2
            left = merge_sort(lst[: mid])
            right = merge_sort(lst[mid:])
            return merge(left, right)
        def merge(left, right): #合并
            l, r, res = 0, 0, []
            while l < len(left) and r < len(right):
                if left[l] <= right[r]:
                    res.append(left[l])
                    l += 1
                else:
                    res.append(right[r])
                    r += 1
            res += left[l:]
            res += right[r:]
            return res
        if tinput == [] or k > len(tinput):
            return []
        tinput = merge_sort(tinput)
        return tinput[: k]

思路8:

//取整除 - 返回商的整数部分(向下取整)
在这里插入图片描述
法复杂度:O(nlogn);

也许有很多同学说,原来也学过很多O(n2)或者O(n3)的排序算法,有的可能优化一下能到O(n)的时间复杂度,但是在计算机中都是很快的执行完了,没有看出来算法优化的步骤,那么我想说有可能是你当时使用的测试用例太小了,我们可以简单的做一下比较:

在这里插入图片描述

当数据量很大的时候 nlogn的优势将会比n2越来越大,当n=105的时候,nlogn的算法要比n2的算法快6000倍,那么6000倍是什么概念呢,就是如果我们要处理一个数据集,用nlogn的算法要处理一天的话,用n2的算法将要处理6020天。这就基本相当于是15年。一个优化改进的算法可能比一个比一个笨的算法速度快了许多,这就是为什么我们要学习算法。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值