【5】数据结构与算法--- 算法 进阶

本文深入讲解了九种经典排序算法,包括冒泡、选择、插入、希尔、快速、归并、堆排序等,分析了各自的特点、应用场景及时间复杂度,并提供了详细的实践代码。

第 4 章 算法 进阶

4.1 排序

4.1.1 排序算法简介

排序:把无序的队列变成有序的队列

排序算法:排序算法是一种将一串无规律数据依照特定顺序进行排列的一种方法或思路

排序算法的稳定性:队列中有相同的元素,排序前后,这两个相同元素的顺序有没有发生变化。

特点:

输入:无序队列 输出:有序队列

应用场景:

各种排行榜 - 服不服排行榜

各种表格 - 座位表

排序算法的关键点:

有序队列:有序区刚开始没有任何数据,逐渐变多

无序队列:有序区刚开始存放素所有数据,逐渐变空

排序算法的稳定性

队列中相同元素排序前后

  • ​ 没有发生变化,这表示算法有稳定性
    ​ 发生变化,表示没有稳定性

常见的排序算法:

三基冒插选,中快高堆归,其基希尔桶,冒快会归并

常见排序算法算法
基础冒泡、插入、选择
中级快速
高级堆、归并
其他基数、希尔、桶
4.1.2 冒泡排序

冒泡排序:

​ 相邻的元素两两比较,升序的话:大的在右,小的在左,降序的话,反之。经过数次比较循环,最终达到一个从小到大(升序)或者从大到小(降序)的有序序列.这个算法由于类似于气泡从水底冒出,所以叫“冒泡”排序。

过程跟踪

​ 在整个冒泡排序过程中,有一个标识指向两个元素的最大值,当这个最大值移动的时候,标识也会随之移动,这就叫做:过程跟踪

特点:

  • - 元素替换:相邻元素从小到大:
    
      		左比右大,数据先交换位置,大的和右侧的元素继续比较
    
      		左比右小,数据不交换位置,大的和右侧的元素继续比较
    
      		左右相等,数据不交换位置,大的和右侧的元素继续比较
    
    - 比较次数:无序队列元素个数 - 1 
    
    - 冒泡次数:无序队列元素个数 - 1 
    
    - 冒泡次数和比较次数关系:
    
      - 冒泡次数:1 ===> n
      - 比较次数:n ===> 1
    
    - 过程跟踪:alist[i]=最大值
    

冒泡排序的分析:

>- 元素替换:最基本元素比较(列表的下表)
>
>- 内层比较循环:每次冒泡排序,内层的元素比较次数
>
>  ​	比较次数 + 元素范围(下标)
>
>- 外层冒泡循环:执行多少次冒泡排序
>
>  ​	冒泡次数、冒泡次数和元素比较次数关系
>
>- 不替换情况:特殊情况
>
>  ​	计数器

冒泡排序的实践:

def bubble_sort(alist):
    """冒泡排序"""
    # 获取列表元素的总数量
    n = len(alist)

    # 3.冒泡排序循环范围
    # 冒泡排序只关注排序次数
    for j in range(n - 1, 0, -1):
        # 开始比较前,定义计数器 count 的初始值为 0
        count = 0

        # 2.内层的数据比较循环范围
        for i in range(j):
            # 1.相邻元素替换
            if alist[i] > alist[i + 1]:
                alist[i], alist[i + 1] = alist[i + 1], alist[i]
                # 数据替换完毕,计数器加 1
                count += 1

        # 4.不替换情况
        # 如果计数器的值为 0,表示没有发生任何替换,那么就退出当前循环
        if count == 0:
            break


if __name__ == '__main__':
    li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(li)
    bubble_sort(li)
    print(li)

时间复杂度

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

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

稳定性:稳定

拓展:降序<

4.1.3 选择排序

简单直观

从无序队列里面挑选最小的元素,和无序队列头部元素替换(放到有序队列中),最终全部元素形成一个有序的队列。

选择排序的原理:

选择排序的主要特点与元素替换有关。

每次移动一个元素,就有一个元素放到有序队列,n个元素的无序队列最多进行(n-1)次交换,就可以形成有序队列。

如果整个队列已排序,那么它不会被移动。

选择排序的分析:

  • 比较循环:无序队列查找最小元素

    ​ mix表示同意最小元素

    ​ cur标识用于遍历所有元素

    ⚠️注意:过程跟踪:mix永远指向最小的,cur指向的元素负责对比

    mix 和 cur 标签的初始化地址是相邻的:

    mix标签所在元素的下标是j,那么cur标签所在元素的下标是 j+1

  • 元素替换:最小元素和无序队列第一个元素替换位置

  • **选择循环:**需要多少次替换,才能形成有序队列

选择排序的实践

def selection_sort(alist):
    """选择排序"""
    # 获取列表元素的总数量
    n = len(alist)

    # 3. 选择循环
    # 定义一个外围循环次数
    # 排序次数范围的确定
    for j in range(n - 1):
        
        # 定义min_index 初始值
        min_index = j

        # 1.比较循环
        # cur 标签元素下标移动范围(1,n-1)
        for i in range(j + 1, n):
            # 找到最小的元素
            if alist[i] < alist[min_index]:
                min_index = i

        # 2.元素替换
        # 保证最新的 min_index 不在无序队列首位,那么就将它和无序队列的首个元素进行替换
        if min_index != j:
            # mix 标签元素和无序队列首位置元素替换
            alist[j], alist[min_index] = alist[min_index], alist[j]


if __name__ == '__main__':
    li = [11, 3, 6, 33, 5, 8, 2, 88]
    print(li)
    selection_sort(li)
    print(li)
关键点:
1、mix标签初始化:min_index = j 
2、比较循环的范围:for i in range(j+1,n) 
3、元素替换的条件:if min_index != j 
4、排序次数范围的确定:for j in range(n-1)

时间复杂度

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

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

稳定性:看情况

4.1.4 插入排序

先定义一个有序队列,然后把无序队列中的第一个元素放到有序队列的 合适位置,重复操作,直至形成一个完整的有序队列

插入排序原理

1、构建有序序列

2、选择无序队列的第一个元素,先放在有序队列末尾,然后进行冒泡排序,放到指定的位置

3、循环2步,直到无序队列中所有元素全部进入有序队列的合适位置

特点:

(1)插入 ?冒泡

  • 联系:插入排序的有序队列用到了冒泡算法

  • 区别:升序情况下,

    ​ 冒泡:有序队列在后,无序队列在前(无序队列 + 有序队列);

    ​ 插入:有序队列在前,无序队列在后(有序队列 + 无序队列)

(2)插入?选择

  • 选择:遍历未排序队列,将最小的元素移动到有序队列的末尾;
  • 插入:把无序队列的第一个元素放到有序队列,通过使用冒泡算法,移动到合适的位置。

插入排序分析

元素替换:有序队列中元素比较替换

比较循环:每次排序,有序队列元素比较替换的次数

排序循环:需要进行多少次排序

插入排序实践

def insert_sort(alist):
    """插入排序"""
    # 无序队列元素数量
    n = len(alist)

    # 3.插入排序循环
    # 有序队列循环的次数
    for i in range(n):

        # 2.比较循环次数的确定
        # 有序队列末尾元素下标为i,范围(0,i]
        for j in range(i, 0, -1):

            # 1.元素替换
            # 有序列表的两个元素进行比较
            if alist[j] < alist[j - 1]:
                # 大小值元素替换
                alist[j], alist[j - 1] = alist[j - 1], alist[j]
            # 条件不满足,大小元素不替换
            else:
                break


if __name__ == '__main__':
    li = [11, 3, 6, 33, 5, 8, 2, 88]
    print(li)
    insert_sort(li)
    print(li)
关键点:
1、元素替换:if alist[j] < alist[j-1] 
2、比较循环:for j in range(i,0,-1) 
3、插入循环:for i in range(n):

时间复杂度

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

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

稳定性:稳定

4.1.5 希尔排序

希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是插入排序算法的一种高效的改进版本

希尔排序原理:

两两一组、四四一组、八八一组…,直到所有元素为一组,进行排序

特点:

下标增量分组,对小组元素进行插入排序

下标增量的特点:
第一次分组,gap=n/2 ,
从第二次分组,gap=gap/2,
最后一次分组gap=1
整个分组过程就是:递归

希尔排序分析:

  • 元素替换:分组队列中元素比较替换

    ​ 下标的范围必须大于0

  • 比较次数:每次分组后,同时有几组在进行比较

    ​ 插入排序

  • 分组次数:需要进行多少次分组

希尔排序实践:

def shell_sort(alist):
    """希尔排序"""
    # 获取列表的长度
    n = len(alist)

    # 3.递归分组循环
    # 获取下标偏移量gap(取整)
    gap = n // 2
    # 只要gap在合理范围内,就一直分组下去
    while gap >= 1:
        
        # 2.比较循环(多少组进行插入排序)
        # 指定i下标的取值范围
        for i in range(gap, n):

            # 1.元素替换
            # 对移动元素的下标进行条件判断
            # 下标的范围必须大于0
            while (i - gap) >= 0:
                # 组内大小元素进行替换
                if alist[i] < alist[i - gap]:
                    alist[i], alist[i - gap] = alist[i - gap], alist[i]
                    # 修改i 的属性重新指向原始元素(过程跟踪)
                    i = i - gap
                # 否则的话,不进行替换
                else:
                    break
        # 没执行完一次分组内的插入排序,对gap进行/2细分
        gap = gap // 2


if __name__ == '__main__':
    li = [11, 3, 6, 33, 5, 8, 2, 88]
    print(li)
    shell_sort(li)
    print(li)
1、元素替换:
    下标范围:while (i - gap) >= 0: 
    替换条件:if alist[i] < alist[i-gap]: 
    过程跟踪:i = i - gap
2、比较循环:
	元素的范围:for i in range(gap,n):
3、递归分组循环
	偏移量初始值:gap = n // 2 
	递归循环的退出条件:while gap >= 1 
	gap偏移量规律:gap = gap // 2

时间复杂度

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

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

稳定性:不稳定

4.1.6 快速排序

快速排序,又称划分交换排序,从无序队列中挑取一个元素,把无序队列分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

挑元素、划分组、分组重复前两步

快速排序原理

挑元素划分组,整体递归分组

特点:

1、因为是无序队列,所以位置可以随机挑

2、临时划分一个空间,存放我们挑选出来的中间元素

3、左标签位置空,移动右标签,反之一样

4、重复3,直到左右侧标签指向同一个位置,

5、把临时存放的中间元素,归位

左手右手一个慢动作,右手左手慢动作重播

整体划分特点:

1、递归拆分

2、拆分到最后,所有小组内的元素个数都是1

递归拆分到不能再拆

快速排序分析

  • 序列切割

    ​ 三个基本标签:

    ​ mid:指定要切割的临时中间数字

    	left:从队列左侧推进的标签 
    

    ​ right:从队列右侧推进的标签

    ​ left永远小于right

    ​ 右侧推进

    ​ 左侧推进

    ​ 停止推进 (即元素归位)

  • 递归切分

    ​ 递归拆分:小组边界的确定 和 递归功能实现

    ​ 左侧边界start:0 右侧边界end:left-1

    ​ 左侧边界start:left+1 右侧边界end:len(alist)-1

    ​ 递归退出条件

快速排序实践

# 2.递归切分
# 2.1 小组边界确定
# 增加两个参数,左边界 start,右边界 end
def quick_sort(alist, start, end):
    """快速排序"""
    # 2.3定义递归退出条件
    if start < end:

        # 1.序列切割
        #  1.1定义三个基本标签
        #  因为 mid 指定的是传入列表的左边界元素
        mid = alist[start]
        left = start
        right = end

        # 定义拆分条件
        while left < right:
            # 1.2 右侧推进
            # 如果right元素 > mid值,right标签左移
            while right > left and alist[right] >= mid:
                right -= 1
            # 如果right元素 < mid值,left标签元素设置为right标签元素
            alist[left] = alist[right]

            # 1.3 左侧推进
            # 如果left元素 < mid值,left标签右移
            while left < right and alist[left] < mid:
                left += 1
            # 如果left元素 > mid值,right标签元素设置为left标签元素
            alist[right] = alist[left]

        # 1.4 停止标签(元素归位)
        # 退出循环,表示 left和right 标签合并在一起了
        # 获取中间值
        alist[left] = mid

        # 2.2 递归功能的实现
        # 函数自调用
        # 对切割后左边的子部分进行快速排序
        quick_sort(alist, start, left - 1)
        #  对切割后右边的子部分进行快速排序
        quick_sort(alist, left + 1, end)


if __name__ == "__main__":
    li = [54, 26, 93, 17, 77, 31, 44, 77, 20]
    print(li)
    quick_sort(li, 0, len(li) - 1)
    print(li)
序列切割:
    1、挑中间元素:mid = alist[start]
    2、右推进:while right > left and alist[right] >= mid: 
    3、左推进:while left < right and alist[left] < mid: 
    4、推进循环:while left < right: 
    5、元素归位:alist[left] = mid
递归拆分:
    1、小组边界确定:left = start、right = end 
    2、递归退出条件:if start < end: 
    3、函数自调用:quick_sort(alist, start, end)

时间复杂度

最优时间复杂度:O(nlogn)

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

稳定性:不稳定

4.1.7 归并排序

归并排序是采用分治法的一个非常典型的应用。

将无序队列拆成两个小组,组内元素排序,然后组间元素逐个比较,把小元素依次放到新队列中。

关键字:拆分、排序、组间、小、新队列

分组排序,合并新队列

归并排序原理

  • 分组排序阶段:

    ​ 1、将无序队列alist,拆分成成两个小组A和B,

    ​ 2、分别对两个小组进行同样的冒泡排序

    ​ 3、用标签left和right,分别对小组A和B进行管理

  • 合并新队列阶段:

    ​ 4、两个标签所在的元素比较大小,

    ​ 5、将小的元素放到一个新队列中,然后小元素所在的标签向右移

    6、多次执行4和5,最终肯定有一个小组先为空
    
    7、把不为空的小组元素,按顺序全部移到新队列的末尾 
    

    ​ 8、无序队列中的所有元素就在新队列中形成有序队列了

特点:

​ 两个阶段:分组排序 + 合并

​ 合并策略:组间比较、新增小,小移标

两种情况:

​ 分两组合并排序

​ 递归分组合并排序:层级分组、排序、层级合并

归并排序分析

  • 分组实现

    ​ 首次分组

    ​ 正常分组

    ​ 不能分组:空队列/队列只有一个元素

    ​ 递归分组

    ​ 合并分组

  • 合并分组的排序

    准备工作

    ​ 组标签:定义两个标签l和r,分别是分组列表的首 位下标值0

    ​ 组长度

    ​ 空队列

    空列表增加数据

    ​ 空列表添加数据

    ​ 一组空,另一组剩余元素按顺序一次性添加到空列

归并排序实践

def fen_zu(alist):
    """分组"""
    # 获取当前序列的长度
    n = len(alist)
    # 1.首次分组
    #   1.2 不能分组
    # 队列异常情况
    if n <= 1:
        return alist
    #   1.1 正常分组
    # 把当前列表分成两部分,使用切片方式h获取两部分内容
    mid = n // 2
    # 左半部分
    # left = alist[:mid]
    # 右半部分
    # right = alist[mid:]

    # 2.递归分组
    # 左半边数据
    zuo = fen_zu(alist[:mid])
    # 右半边数据
    you = fen_zu(alist[mid:])

    # 3.合并分组
    # 将分组后的数据交个一个合并数据的函数去处理
    return merge(zuo, you)


def merge(zuo, you):
    """归并排序"""

    # 3.1 准备工作
    # (1)组标签
    # 定义标签 l 和 r 在两组的位置
    l, r = 0, 0
    # (3)空队列
    # 定义一个空列表
    result = []
    # (2)组长度
    # 获取两个分组的长度
    zuo_len = len(zuo)
    you_len = len(you)

    # 指定标签的有效范围
    while l < zuo_len and r < you_len:
        # 3.2 空列表添加数据
        # 判断两侧标签指定的数据大小
        if zuo[l] <= you[r]:
            # 将左侧小数据追加到新队列
            result.append(zuo[l])
            # left标签右移一位
            l += 1
        else:
            # 将右侧小数据追加到新队列
            result.append(you[r])
            # right标签右移一位
            r += 1

    # 3.3 一组为空,另一组剩余元素按顺序添加到新队列
    # 将左侧的剩余内容,一次性添加到 result 表中
    result += zuo[l:]
    # 将右侧的剩余内容,一次性添加到 result 表中
    result += you[r:]

    # 返回 result 表
    return result


if __name__ == "__main__":
    li = [54, 26, 93, 17, 77, 31, 44, 77, 20]
    print("处理前: %s" % li)
    sort_list = fen_zu(li)
    print("处理后: %s" % li)
    print("新列表: %s" % sort_list)
关键点:
1、异常分组:if n <= 1: 
2、递归分组:fen_zu(alist[:mid]) 
3、分组合并:merge(zuo, you)
	1、数据比较条件:while l < zuo_len and r < you_len: 
	2、小元素移动:result.append(zuo[l]) 
	3、小元素标签处理:l += 1
	4、异常情况:result += zuo[l:]
	5、最终效果:return result

时间复杂度

最优时间复杂度:O(nlogn)

最坏时间复杂度:O(nlogn)

稳定性:稳定

4.1.8 堆排序

堆是采用顺序表存储的一种近似完全二叉树的结构。

父节点和子结点关系(父找子):

左子节点位置:2i + 1 右子节点位置:2i + 2

堆分类

堆分类
大顶堆任一节点都比其孩子节点大
最大值 堆顶元素 alist[0]
小顶堆任一节点都比其孩子节点小

它是指利用堆这种树结构所设计的一种排序算法。

将无序列表先构造一个有特点的堆,然后利用列表的特点快速定位最大/小的元素,将其放到一个队列中。

特点:

无序队列构建一个堆,堆顶和堆尾元素替换位置

重新构建堆,堆顶和堆尾元素替换位置,…

头尾替换,恢复堆后再继续

堆排序原理

构建一个堆:从最后一个有子节点的节点开始构建,下标为[n/2-1]

堆的调整:移除堆顶元素,用队列中最后一个元素填补,自上而下进行调整

​ 1 将无序列表构造为一个标准的大顶堆
2 将堆顶元素和堆尾元素进行替换
相当于将最大元素放到了有序序列
剩余的无序序列少了一个
3 将剩余的无序序列重新调整为标准的大顶堆
4 重复2-3 最终形成一个有序序列

堆排序分析

堆的调整:

​ 准备工作

​ 队列参数:data

​ 堆顶元素的确定:传入一个堆顶队列的下标low

​ 堆顶元素的临时存放空间:tmp

​ 无序队列中元素的最大下标值:high

​ 选择新的堆顶节点

​ 选大的子节点

​ 最大子节点跟移除的堆顶元素进行比较

堆顶元素排序

​ 堆的构造

堆顶元素输出到有序队列

		步骤 
				1 无序序列构造标准大顶堆
				2 堆顶和堆尾元素替换
				3 剩余无序序列调整大顶堆
				4 重复2-3 

堆排序实践

关键点
				1 无序序列构造标准大顶堆
					从最后一个包含子节点的父节点开始构造
						n/2 -1
						顺序:从下向上来构造的
						n/2 -1  n/2 -2   n/2 -3 。。。0 
						表达式:
							range(int(n/2)-1,-1,-1)
				2 堆顶和堆尾元素替换
					无序列表是alist
					堆顶位置  0
					假设堆尾下标是z 
					替换:	
						alist[0],alist[z] = alist[z],alist[0]
					
				3 剩余无序序列调整大顶堆
					3.1 把临时堆顶元素放到临时空间tmp
						tmp = alist[0]
					3.2 选择一个最大的子节点
					3.3 最大子节点和临时堆顶元素比较
						如果临时堆顶元素大,那么归位
						如果子节点大,那么最大子节点放到堆顶位置
							假设子节点下标为j  alist[0] = alist[j]
						3.4 沿着破坏的路径继续调整下去
							i = j
							j = 2*i + 1
					原则:
						从上到下
					调整的时候,:	
						对象 alist 
						堆顶位置 low
						调整的范围 	high
							
				4 重复2-3
					在替换的过程中,堆尾元素的下标z变化是:
						n-1 n-2 n-3 n-4 ..... 0 
						表达式:
							range(n-1,-1,-1)
def sift(alist, low, high):
    """堆的调整"""
    # 1.准备工作:堆顶元素的标签 + 临时队列
    # 指定移除的堆顶位置元素下标为 i
    i = low
    # 将移除的堆顶元素存放到一个临时队列tmp
    tmp = alist[i]

    # 2.选择新的堆顶节点
    # 假设左子节点是大节点,下标是 j
    j = 2 * i + 1
    # j 下标的操作范围
    # 无序队列中元素的最大下标值
    # 左侧子节点小于堆的最大范围值
    while j <= high:
        # 2.1 选取最大子节点
        # 左右子节点进行比较
        if j + 1 <= high and alist[j] < alist[j + 1]:
            # 过程跟踪,保证通过 j 找到最大元素
            # 上移节点标号 j 指向右侧节点
            j += 1

        # 2.2 最大子节点 和 原堆顶节点比较
        # 子结点 > 原堆顶节点
        if alist[j] > tmp:
            # 将子节点元素移动到堆顶元素
            alist[i] = alist[j]
            # 因为子节点位置空了,相当于堆顶节点移除了,又要重复操作,所以需要更新 i 和 j 的值
            i = j
            j = 2 * i + 1

        # 如果最大的子节点小于移除的堆顶元素,终止该操作即可
        else:
            break

    # 临时堆顶元素归位
    # 设置堆顶节点为原来的内容
    alist[i] = tmp


def heap_sort(alist):
    """堆排序"""
    # 获取当前列表长度
    n = len(alist)

    # 1.无序列表构造标准大堆顶
    # 对所有父节点进行堆的调整,而且是降序排列(从下往上构造)
    # 最后一个元素是n,其父元素尾n/2-1
    for i in range(int(n / 2) - 1, -1, -1):
        sift(alist, i, n - 1)

    # 堆顶元素排序
    # 指定最小元素的范围
    for z in range(n - 1, -1, -1):
        # 2.堆顶元素与堆尾元素替换
        # 队列中最大元素和最小元素进行替换
        alist[0], alist[z] = alist[z], alist[0]
        # 3.剩余无序列表调整大顶堆(从上到下)
        # 调整新队列
        # 替换完毕后,重新调整堆结构,新的堆结构元素个数变成了 i-1 个
        sift(alist, 0, z - 1)
    # 返回最终的有序队列
    return alist


if __name__ == '__main__':
    a = [0, 2, 6, 98, 34, 5, 23, 11, 89, 100, 7]
    print("排序之前:%s" % a)
    c = heap_sort(a)
    print("排序之后:%s" % c)

时间复杂度

成本:

​ 最优: O(nlogn)

​ 最坏: O(nlogn)

稳定性:不稳定

4.1.9 排序总结
		技术:

				冒小左移,选追加

				插入合适,分希尔

				快速两半,归新列

				顺表构造首尾堆
		
		成本:
			冒泡  插入 选择 希尔 堆 归并 快速  系统

技术总结

冒泡排序在无序队列中选择最小的移动到最左侧
选择排序定一个有序队列,从无序队列中选择最小的元素追加到有序队列的末尾
插入排序定一个有序队列,从无序队列中选择第一个元素,插入到到有序队列的合适位置
希尔排序通过对无序队列进行分组,然后再采用插入的排序方法
快速排序指定一个元素,将无序队列拆分为大小两部分,然后层级递进,最终实现有序队列
归并排序是将无序队列拆分,然后小组内排序,组间元素比较后在新队列中进行排序
堆 排 序顺序表方式构造堆,首尾替换调整堆
冒小左移 选追加,插入合适 分希尔,快速两半 归新列,顺表构造首尾堆

成本总结

排序方法时间复杂度稳定性代码复杂度
最坏情况平均情况最好情况
冒泡排序O(n2)O(n^2)O(n)稳定简单
选择排序O(n2)O(n^2)O(n^2)不稳定简单
插入排序O(n2)O(n^2)O(n^2)稳定简单
希尔排序O(n2)O(nlogn~n^2)O(nlogn~n^2)不稳定中下等
堆排序O(nlogn)O(nlogn)O(nlogn)不稳定中等
归并排序O(nlogn)O(nlogn)O(nlogn)稳定中等
快速排序O(n2)O(nlogn)O(nlogn)不稳定中下等

4.2 搜索

4.2.1 搜索简介

搜索是在队列中找到一个特定元素的算法过程。

搜索的常见方法:

​ 顺序查找 / 二分法查找 /二叉树查找 / 哈希查找

4.2.2 二分查找

简介

二分查找又称折半查找,适用于不经常变动查找频繁有序列表。

优点:比较次数少、查找速度快、平均性能好

缺点:要求待查表为有序表,且插入删除困难

原理

1.找中间,比较

2.二分比较

3、重复1-2过程,如果最终能找到,表示查找成功True,否则的话,查找不成功False

4.2.3 递归二分实践

递归二分查找分析

列表是否为空

列表中间元素匹配

​ 匹配成功

​ 匹配失败

递归二分查找

二分查找实践

def binary_search(alist, item):
    """二分查找"""
    # 获取列表的长度
    n = len(alist)
    # 1.列表是否为空
    if n == 0:
        return False
    
    # 2.列表中间元素匹配
    # 列表从中间切开
    mid = n // 2
    #  2.1判断是否匹配
    # 匹配成功
    # 判断中间值是否是我们想要的值,是的话返回 True
    if alist[mid] == item:
        return True

    #  2.2匹配失败
    # 查找的元素小于队列中间值
    elif item < alist[mid]:
        # 3.二分查找
        # 左侧二分递归查找
        return binary_search(alist[:mid], item)
    # 查找的元素大于队列中间值
    else:
        # 右侧二分递归查找
        return binary_search(alist[mid + 1:], item)


if __name__ == '__main__':
    testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42]
    print(binary_search(testlist, 3))
    print(binary_search(testlist, 13))

4.3 二叉树

4.3.1 二叉树遍历

遍历简介

遍历是指对树中所有结点的信息的访问一次且仅访问一次

遍历方法

树的遍历模式特点
广度优先遍历(队列)查看数据:
从上到下,分层查看,每层从左向右依次查看,直至所有数据查看完毕
添加数据:
从上到下,分层添加,每层从左向右依次添加
深度优先遍历(递归)首先递归方法看最深的分支元素,
再看其他的节点元素。
三种深度遍历方法:
先序遍历(preorder)访问顺序:根节点->左子树->右子树
中序遍历(inorder)访问顺序:左子树->根节点->右子树
后序遍历(postorder)访问顺序:左子树->右子树->根节点
4.3.2 查询实践(广度优先)

二叉树没有数据

二叉树有数据

class BaseNode(object):
    """定义节点的基本属性"""

    def __init__(self, item):
        # 节点存储的内容
        self.item = item
        # 左侧子节点的索引值
        self.lsub = None
        # 右侧子节点的索引值
        self.rsub = None


class Tree(object):
    """二叉树遍历"""

    def __init__(self, node=None):
        """定义树的基本属性:根节点"""
        # 不能写成self.__root,因为肯定需要看
        self.root = node

    # 添加结点
    def add(self, elem):
        # 新节点
        # 定义要添加到树结构中的节点信息
        node = BaseNode(elem)

        # 1.如果根结点不存在数据
        # 如果树是空,则对根节点赋值,对应的特点:从上到下
        if self.root == None:
            self.root = node

        # 如果根节点存在数据
        else:
            # 2.待处理父节点队列
            # 使用一个临时列表来存储我们要处理的元素: 对应的特点-从左到右(待处理父节点队列)
            queue = []
            # 先把根节点放到我们要处理的临时队列中
            queue.append(self.root)

            # 给父节点添加数据
            # 只要处理队列中有要处理的数据,那就要一直处理下去
            while queue:

                # 3.确定要操作的父节点
                # 从队列的头部获取要处理的元素
                cur = queue.pop(0)

                # 4.左侧子结点是否有数据
                # 判断要处理的左侧子节点为空
                if cur.lsub == None:
                    # 把接受到的item放到左侧节点位置
                    cur.lsub = node
                    # 添加完毕后,退出
                    return
                # 左侧子节点不为空,有数据
                # 左侧节点有数据,把该数据追加到待处理父节点队列末尾
                else:
                    queue.append(cur.lsub)

                # 5.右侧子结点是否为空
                if cur.rsub == None:
                    # 把接受到的item放到右侧节点位置
                    cur.rsub = node
                    # 添加完毕后,退出
                    return
                # 右侧子节点不为空,有数据
                # 右侧节点有数据,把该数据追加到待处理父节点队列末尾
                else:
                    queue.append(cur.rsub)
def breadth_search(self):
    """二叉树广度优先遍历"""
    # 1.二叉树没有数据
    if self.root is None:
        return
    # 2.二叉树有数据
    else:
        # 2.1创建一个待处理父节点队列
        queue = []
        # 把根节点放入
        queue.append(self.root)

        # 2.2 判断临时队列是否为空
        # 判断临时队列不为空,表示还有未查看的父节点
        while queue:
            # 获取父节点并打印信息
            node = queue.pop(0)
            print(node.item, end=' ')
            # 左侧节点不为空,将其放入待处理队列
            if node.lsub is not None:
                queue.append(node.lsub)
            # 右侧节点不为空,将其放入待处理队列
            if node.rsub is not None:
                queue.append(node.rsub)
        # 修复 print 功能
        print("")
关键点
    1、空树处理: if self.root == None: 
    2、树非空,待处理父节点队列 queue.append(self.root) 
    3、左右侧子节点处理:
            不空,将其放入待处理父节点队列:queue.append(node.lsub)

4.3.3 查询实践(深度优先)

递归查询

先序遍历

pdef preorder(self, root):
    """
    深度优先遍历(1):先序遍历
    根节点===>左节点===>右节点
    """
    # 根节点没有数据,为空
    if root is None:
        return
    # 根节点有数据
    # 打印根节点内容
    print(root.item, end=' ')
    self.preorder(root.lsub)
    self.preorder(root.rsub)

中序遍历

def inorder(self, root):
    """
    深度优先遍历(1):中序遍历
    左节点===>根节点===>右节点
    """
    # 根节点没有数据,为空
    if root is None:
        return
    # 根节点有数据
    # 打印根节点内容
    self.inorder(root.lsub)
    print(root.item, end=' ')
    self.inorder(root.rsub)

后序遍历

def postorder(self, root):
    """
    深度优先遍历(1):后序遍历
    左节点===>右节点===>根节点
    """
    # 根节点没有数据,为空
    if root is None:
        return
    # 根节点有数据
    # 打印根节点内容
    self.postorder(root.lsub)
    self.postorder(root.rsub)
    print(root.item, end=' ')
4.3.4 二叉树反推(拓展)

反推原理:先定边,往复树两边

先序 + 中序 / 中序 + 后序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值