算法之列表的排序

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

一、列表的查找

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

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

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

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


排序方法总结比较
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,535评论 0 13
  • 排序的基本概念 在计算机程序开发过程中,经常需要一组数据元素(或记录)按某个关键字进行排序,排序完成的序列可用于快...
    Jack921阅读 1,361评论 1 4
  • 1)这本书为什么值得看: Python语言描述,如果学的Python用这本书学数据结构更合适 2016年出版,内容...
    孙怀阔阅读 12,286评论 0 15
  • 女人总是爱梦想,梦想找到真爱。其实,到最后,大都数女人找到的是性爱,而不是爱情。 世上能有几个男人有始有终坚持爱一...
    A寒秋阅读 227评论 0 3
  • 虽然很不想承认,但不能否定这是一次很尴尬的笔记,起因是我们班内分享后,主动链接了分享嘉宾,冠冕堂皇的夸战友分享的好...
    瓶子笔记本阅读 249评论 0 1