算法之列表的排序

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

一、列表的查找

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

两种方式:
顺序查找:从列表第一个元素开始,顺序进行搜索,直到找到为止。时间复杂度是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)

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

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

《算法之列表的排序》 排序方法总结比较

    原文作者:SlashBoyMr_wang
    原文地址: https://www.jianshu.com/p/b571bfea4aa2
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞