美文网首页python全栈学习
算法之列表的排序

算法之列表的排序

作者: SlashBoyMr_wang | 来源:发表于2018-10-26 20:53 被阅读12次

    在探讨列表的排序之前,我们先来了解一下列表的查找问题吧!

    一、列表的查找

    列表查找:顾名思义就是从列表中查找指定元素
    函数要求:
    输入列表、待查找元素,输出元素下标或未查找到元素

    两种方式:
    顺序查找:从列表第一个元素开始,顺序进行搜索,直到找到为止。时间复杂度是O(n)
    二分查找:从有序列表的候选区data[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。时间复杂度是O(logn)

    顺序查找代码示例:

    def linear_search(li, val):
        for index in range(len(li)):
            if li[index] == val:
                return index
        else:
            return None
    

    二分查找代码示例:

    def bin_search(li, val):
        low = 0
        high = len(li) - 1
        while low <= high:
            mid = (low + high) // 2
            if li[mid] == val:
                return mid
            elif li[mid] > val:
                high = mid - 1
            else:
                low = mid + 1
    

    递归版本的二分查找

    def bin_search_revc(li, val, low, high):
        if low <= high:
            mid = (low + high) // 2
            if li[mid] == val:
                return mid
            elif li[mid] > val:
                return bin_search_revc(li, val, low, mid - 1)
            else:
                return bin_search_revc(li, val, mid + 1, high)
        else:
            return None
    

    二、列表排序---冒泡排序、选择排序、插入排序

    列表排序的目的:
    将无序列表变为有序列表

    应用场景:
    1.各种表单,排行榜,热搜榜等
    2.各种表格,数据处理
    3.给二分查找有序结果
    4.其他运算

    列表排序的两个关键词:有序区无序区

    三种基本的排序方式:
    1.冒泡排序:列表每两个相邻的数比较大小,如果前边的比后边的大,那就互换位置。就像冒泡。
    2.选择排序:一趟遍历记录最小的数,放到第一个位置,再一趟遍历记录剩余列表中最小的数,继续放置
    3.插入排序:列表被分为有序区和无序区两个部分。最初有序区只有一个元素。每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。

    冒泡排序动图演示:


    冒泡排序.gif

    冒泡排序代码:时间复杂度O(n²)
    两层for循环,第一层循环控制总趟数,第二层循环无序区,比较大小,交换位置

    def bubble_sort(li):
        # i表示第i趟,共n-1趟
        for i in range(len(li) - 1):
            # 第i趟 无序区范围 0~n-i-1
            flag = False
            for j in range(len(li) - i - 1):
                if li[j] > li[j + 1]:
                    li[j], li[j + 1] = li[j + 1], li[j]
                    flag = True
            # 如果一趟下来列表的顺序没有任何改变,
            # 证明已经是有序的了,直接结束函数
            if not flag:
                return
    

    选择排序代码:O(n²)
    两层for循环,第一层循环控制总趟数,默认最小值是无序区第一个数,第二层循环无序区,寻找并记录最小值位置,第二层循环结束交换两个位置。

    def select_sort(li):
        # i 表示第i趟 无序区的范围 i ~ n-1
        for i in range(len(li) - 1):
            # 默认最小值是无序区第一个数
            min_loc = i
            # 循环查找无序区的最小值,并记录位置
            for j in range(i + 1, len(li)):
                if li[j] < li[min_loc]:
                    min_loc = j
            # 将本趟无序区的第一个位置和无序区最小值交换位置
            if min_loc != i:
                li[i], li[min_loc] = li[min_loc], li[i]
    

    插入排序动图演示:


    插入排序.gif

    插入排序代码:O(n²)
    默认第一个值为有序区,第一层for循环控制总趟数,获取无序区第一个值为tem,while循环依次比较tem和有序区的值并依次向后移动一位,最后将tem插入对应位置。

    def insert_sort(li):
        for i in range(1, len(li)):
            tem = li[i]
            j = i - 1
            while j > -1 and tem < li[j]:
                li[j], li[j + 1] = li[j + 1], li[j]
                j -= 1
            else:li[j+1] =tem
    

    三、列表排序---快速排序、堆排序、归并排序

    快排的特点:快、效率高

    快排思路:
    1.取一个元素p(第一个元素),使元素p归位;
    2.列表被p分成两部分,左边都比p小,右边都比p大;
    3.递归完成排序。

    快排的时间复杂度:O(nlogn)

    def partition(li, left, right):
        # 默认取第一个数P进行归位
        tem = li[left]
        while left < right:
            # left,right两个指针分别从两边相向查找,右边小于P的值交换到左边
            while left < right and li[right] >= tem:
                right -= 1
            li[left] = li[right]
            # 左边大于P的值交换到右边
            while left < right and li[left] <= tem:
                left += 1
            li[right] = li[left]
        # 直到left、right两个指针重合,将p值插入完成
        li[right] = tem
        return right
    
    def quick_sort(li, left, right):
        if left < right:
            mid = partition(li, left, right)
            quick_sort(li, left, mid - 1)
            quick_sort(li, mid + 1, right)
    
    快排递归中的partition.gif

    四、堆排序前传——树与二叉树简介

    树的定义:
    树是一种可以递归定义的数据结构,是由n个节点组成的集合:当n=0,那这是一棵空树;当n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树。
    树的应用:目录,评论等

    树的一些概念:
    根节点:树的一个组成部分,也叫树根。所有非空的二叉树中,都有且仅有一个根结点。它是同一棵树中除本身外所有结点的祖先,没有父结点。
    叶子节点:一棵树当中没有子结点(即度为0)的结点称为叶子结点。
    树的深度(高度):树中节点的最大层次。
    树的度:一个节点含有的子树的个数称为该节点的度,最大的节点的度称为树的度。
    孩子节点/子节点:一个节点含有的子树的根节点称为该节点的子节点。
    父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。
    子树:设T是有根树,a是T中的一个顶点,由a以及a的所有后裔(后代)导出的子图称为有向树T的子树。

    树的一些概念

    特殊且常用的树—二叉树
    二叉树:度不超过2的树(节点最多有两个叉)

    两种特殊二叉树:
    满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
    完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。

    两种特殊二叉树

    二叉树的存储方式:
    1.链式存储方式
    2.顺序存储方式(列表)
    父节点和左孩子节点的编号下标关系:父节点为 i ---> 左孩子节点 2i+1
    父节点和右孩子节点的编号下标关系:父节点为 i---> 左孩子节点 2i+2

    父子节点关系

    五、堆排序


    大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大
    小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小

    大根堆小根堆示例

    堆的向下调整性质:
    当根节点的左右子树都是堆时,可以通过一次向下的调整来将其变换成一个堆。

    堆的向下调整性质.gif

    堆排序过程
    1.建立堆
    2.得到堆顶元素,为最大元素
    3.去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序。
    4.堆顶元素为第二大元素。
    5.重复步骤3,直到堆变空。

    堆调整排序过程.gif

    堆排序代码实现:

    def sift(li, low, high):
        """
        堆的调整函数
        :param li:列表
        :param low: 根节点
        :param high: 堆最后一个元素位置
        """
        tmp = li[low]
        i = low
        j = 2 * i + 1
        while j <= high:  # 第一个退出条件:j不存在
            if j + 1 <= high and li[j + 1] > li[j]:  # 如果右孩子存在并且右孩子比左孩子大
                j += 1  # j指向右孩子
            if li[j] > tmp:
                li[i] = li[j]
                i = j
                j = 2 * i + 1
            else:  # 第二个退出条件:j位置的值比tmp小
                break
        li[i] = tmp
    
    def heap_sort(li):
        # 1. 建大根堆
        n = len(li)
        for i in range(n // 2 - 1, -1, -1):  # i表示遍历的low
            sift(li, i, n - 1)  # low:i high:统一写成最后一个位置对正确性没有影响
        # 2. 挨个出数:退休 棋子 调整
        for i in range(n - 1, 0, -1):  # i 表示当前位置堆的high
            li[i], li[0] = li[0], li[i]  # 根节点也就是最大的数放在最后面
            sift(li, 0, i - 1)  # 重新调整
    

    六、堆排序—内置模块 heapq

    优先队列:
    一些元素的集合,POP操作每次执行都会从优先队列中弹出最大(或最小)的元素。
    内置模块 heapq就是利用的优先队列来实现的

    Python内置模块—heapq
    1.heapify(x) === 建堆,将x转成小根堆形式。
    2.heappush(heap, item) === 向heap中添加item,heap一直维持着小根堆的秩序。
    3.heappop(heap) === 从heap中pop一个元素,heap一直维持着小根堆的秩序。

    利用heapq模块实现堆排序

    def heap_sort(li):
        L = []
        for i in range(len(li)):
            heapq.heappush(L,li[i])
        return [heapq.heappop(L) for i in range(len(L))]
    

    七、堆排序扩展——topK问题

    需求:现在有n个数,设计算法找出前k大的数(k<n)

    解决方法:
    1.排序后切片
    2.堆的应用

    堆排序实现TopK问题:
    解决思路:
    1.取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数。
    2.依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整。
    3.遍历列表所有元素后,倒序弹出堆顶。

    堆排序实现TOPK问题.gif

    堆排序实现TOPK问题代码:

    def sift(li, low, high):
        """
        堆的调整函数
        :param li:列表
        :param low: 根节点
        :param high: 堆最后一个元素位置
        """
        tmp = li[low]
        i = low
        j = 2 * i + 1
        while j <= high:  # 第一个退出条件:j不存在
            if j < high and li[j + 1] < li[j]:  # 如果右孩子存在并且右孩子比左孩子大
                j += 1  # j指向右孩子
            if li[j] < tmp:
                li[i] = li[j]
                i = j
                j = 2 * i + 1
            else:  # 第二个退出条件:j位置的值比tmp小
                break
        li[i] = tmp
    
    
    def topk(li, k):
        # 取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数。
        heap = li[0:k]
        for i in range(k // 2 - 1, -1, -1):
            sift(heap, i, k - 1)
        # 依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;
        # 如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整。
        print("==", heap)
        for i in range(k, len(li)):
            if li[i] > heap[0]:
                heap[0] = li[i]
                print("前==", heap)
                sift(heap, 0, k - 1)
                print("后==", heap)
        # 遍历列表所有元素后,倒序弹出堆顶
        for i in range(k - 1, -1, -1):
            heap[0], heap[i] = heap[i], heap[0]
            sift(heap, 0, i - 1)
        return heap
    

    八、归并排序

    假设现在的列表分两段有序,如何将其合成为一个有序列表???

    这种操作称为一次归并。 一次归并.gif

    一次归并代码实现:

    def merge(li, low, mid, high):
        # 左半边有序列表第一位
        i = low
        # 右半边有序列表第一位
        j = mid + 1
        li_tmp = []
        # 如果两边都有数
        while i <= mid and j <= high:
            #  左边小于右边
            if li[i] < li[j]:
                li_tmp.append(li[i])
                i += 1
            else:
                li_tmp.append(li[j])
                j += 1
        # 判断左边列表是否有剩余元素
        while i <= mid:
            li_tmp.append(li[i])
            i += 1
        # 判断右边列表是否有剩余元素
        while j <= high:
            li_tmp.append(li[j])
            j += 1
        # 将新生成列表赋值回原来列表
        for i in range(len(li_tmp)):
            li[low + i] = li_tmp[i]
    

    一次归并练习:
    有两个有序的列表,通过代码合并成一个有序列表:代码如下:

    def marge(li1, li2):
        i = 0
        j = 0
        ret_list = []
        while i < len(li1) and j < len(li2):
            if li1[i] < li2[j]:
                ret_list.append(li1[i])
                i += 1
            else:
                ret_list.append(li2[j])
                j += 1
        while i < len(li1):
            ret_list.append(li1[i])
            i += 1
        while j < len(li2):
            ret_list.append(li2[j])
            j += 1
        return ret_list
    

    归并排序步骤:
    分解:将列表越分越小,直至分成一个元素。
    终止条件:一个元素是有序的。
    合并:将两个有序列表归并,列表越来越大。

    归并排序步骤分解

    归并排序代码实现:时间复杂度:O(nlogn)

    def merge(li, low, mid, high):
        # 左半边有序列表第一位
        i = low
        # 右半边有序列表第一位
        j = mid + 1
        li_tmp = []
        # 如果两边都有数
        while i <= mid and j <= high:
            #  左边小于右边
            if li[i] < li[j]:
                li_tmp.append(li[i])
                i += 1
            else:
                li_tmp.append(li[j])
                j += 1
        # 判断左边列表是否有剩余元素
        while i <= mid:
            li_tmp.append(li[i])
            i += 1
        # 判断右边列表是否有剩余元素
        while j <= high:
            li_tmp.append(li[j])
            j += 1
        # 将新生成列表赋值回原来列表
        for i in range(len(li_tmp)):
            li[low + i] = li_tmp[i]
    
    def _merge_sort(li, low, high):
        if low < high:  # 至少两个元素
            mid = (low + high) // 2
            _merge_sort(li, low, mid)
            _merge_sort(li, mid + 1, high)
            merge(li, low, mid, high)
    

    补充:在列表元素10万左右进行排序时,使用系统的sort()排序所需时间在0.03s-0.05s;使用python实现的快排所需时间在0.4-0.5s之间;堆排序在0.65s-0.7s之间;归并排序在0.65s-0.7s之间(时间长短因为电脑配置不同有差异)。这是因为系统的sort排序的底层是 C语言实现的,从而可以知道python语言的执行效率相对C语言的执行效率要低很多。

    快排、堆排、归并三种排序算法的时间复杂度都是O(nlogn)

    一般情况下,就运行时间而言:快速排序 < 归并排序 < 堆排序

    三种排序算法的缺点:
    快速排序:极端情况下排序效率低
    归并排序:需要额外的内存开销
    堆排序:在快的排序算法中相对较慢


    排序方法总结比较

    相关文章

      网友评论

        本文标题:算法之列表的排序

        本文链接:https://www.haomeiwen.com/subject/slwqtqtx.html