数据结构和算法(Python)

数据结构

基础

算法的概念和特性

  • 程序:数据结构 + 算法
  • 数据结构:存储和组织数据的方式
  • 算法:解决问题的思维,思路,方式

五大特性

算法是独立解决问题的思想和方法

  1. 有输入:算法具有0个或多个输入
  2. 有输出:算法至少有一个或多个输出
  3. 又穷性:算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成
  4. 确定性:算法中的每一步都有确定的含义,不会出现二义性
  5. 可行性:算法的每一步都是可行的,每一步都能够执行有限的次数完成

判断算法的优劣

优化前

py 复制代码
import time

start = time.time()

for i in range(0, 1001):
    for o in range(0, 1001):
        for e in range(0, 1001):
            if i + o + e == 1000: and i ** 2 + o ** 2 == e ** 2 # 65
            # if i ** 2 + o ** 2 == e ** 2 and i + o + e == 1000: # 565
                print(i, o, e)

end = time.time()
print(end - start)

优化后

py 复制代码
import time

start = time.time()

for i in range(0, 1001):
    for o in range(0, 1001):
        c = 1000 - i - o
        if i ** 2 + o ** 2 == c ** 2: # 0.5
            print(i, o)

end = time.time()
print(end - start)

判断算法优劣的方式

下图:1001³ * 9 * 每步执行时间

下图:1001² * 9 * 每步执行时间

时间复杂度

大O标记法

时间复杂度

适用于衡量算法的优劣的,一般采用大O标记法,把次要条件都忽略,最终形成的表达式就叫大O标记法

  • 问题规模:程序(算法)的计算量,计算范围
  • 主要条件:随着问题规模变化而变化的代码
  • 次要条件:随着问题规模变化而不变的代码

计算规则

  1. 基本操作,时间复杂度是O(1)
  2. 顺序结构,时间复杂度按加法计算,比如 o(1) + o(1) = o(1)
  3. 循环结构,时间复杂度按乘法计算,比如 o(n) * o(n) * o(n) = o(n³)
  4. 分支结构:时间复杂度取最大值,比如,if 的结果是 o(o²) else o(n) 结果是o(o²)
  5. 判断算法时,往往关注操作数量的最高次项,其他次要项可以忽略
  6. 没有特殊,分析算法都是指最坏时间复杂度

如何分析

分析的时候,只参考主要条件,忽略次要条件和常数项。

如果没有特别的说明,我们分析时间复杂度,指的是最坏的时间复杂度。

  • 最优时间复杂度:最理想,最乐观,算法需要的最少步骤
  • 最坏时间复杂度:算法的保障,最多需要多少步骤能计算出结果。

常见的时间复杂度

执行次数函数举例 非正式术语
12 O(1) 常数阶
2n+3 O(n) 线性阶
3n2+2n+1 O(n2) 平方阶
5log2n+20 O(logn) 对数阶
6n3+2n2+3n+4 O(n3) 立方阶

所消耗的时间从小到大:

O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3)

时间复杂度越低,效率越高

备注:

  • O(logn):大O计数法:时间T与问题的规模变化曲线;二分法
  • O(nlogn):一个for循环是n 另外一个for循环是二分法,组合在一起

什么是空间复杂度

空间复杂度是对一个算法,在运行过程中临时占用存储空间大小的度量

类似于时间复杂度,一个算法的空间复杂度S(n)定义为该算法所耗费的存储空间,也使用大O标记法。

常见的有

O(1) < O(logn) < O(n) < O(n2) < O(n3)

常见判断法

常数阶(o¹)

因为图中的 0,无论递增到多少,永远都是会用一个空间来覆盖另一个空间,所以是 0¹

线性阶(oⁿ)

每次循环一个数据,空间都会持续增加,就是线性阶

平方阶 o(n²)

对数阶O(log₂ n)

数据结构

内存是以字节为基本存储单位的, 每个基本存储空间都有自己的地址(注意:一个内存地址代表一个字节(8bit)的存储空间)。

  • 整形(int): 4个字节
  • 字符(char): 1个字节 . 单个字符"a"占1个字节, 字符串"abc"占3个字节

线性结构和非线性结构

线性结构

线性结构的特点:

  1. 线性结构是非空集
  2. 线性结构所有结点都最多只有一个直接前驱结点和一个直接后继结点

非线性结构

非线性结构的特点:

  1. 非线性结构是非空集
  2. 非线性结构的一个结点可能有多个直接前驱结点和多个直接后继结点

线性结构存储方式

线性结构的实际存储方式,分为两种:

  1. 顺序表 : 将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示
  2. 链表 : 将元素存放在通过链接构造起来的一系列存储块中 , 存储区是非连续的

顺序表元素顺序地存放在一块连续的存储区里,具体的存储方式的两种情况:

  • 一体式结构

必须是有序的线性结构,元素之间没有间隙,并且可以通过索引访问,元素的占用空间是一样的。

  • 分离式结构

地址的大小为4字节是固定的 , 我们可以不存储数据 , 而是存储地址,这样就解决了,占用空间大小不一样的问题。左边存地址,右边存数据。

无论一体式结构还是分离式结构,顺序表在获取数据的时候直接通过下标偏移就可以找到数据所在空间的地址 , 而无需遍历后才可以获取地址 . 所以顺序表在获取地址操作时的时间复杂度 : O(1)

顺序表

存储结构

顺序表的完整信息包括两部分:

  1. 数据区
  2. 信息区(元数据),即元素存储区的容量和当前表中已有的元素个数 (大白话:信息区对数据进行描述、信息显示)

扩充

时空转换

扩充的两种策略

  1. 每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长

特点:节省空间,但是扩充操作频繁,操作次数多。

  1. 每次扩充容量加倍,如每次扩充增加一倍存储空间。

特点:减少了扩充操作的执行次数,但可能会浪费空间资源 , 以空间换时间,推荐的方式

如果是一体式的,在扩容的时候,需要整体搬迁。

如果是分离式的,在扩容的时候,只需要搬迁数据区,重新关联信息区就可以了。

顺序表增加元素 / 删除元素

增加元素

  • a. 尾端加入元素,时间复杂度为O(1)
  • b. 非保序的加入元素(不常见),时间复杂度为O(1) eg:在指定位置1号位置加入111元素
  • c. 保序的元素加入,时间复杂度为O(n)

删除元素

  • a. 删除表尾元素,时间复杂度为O(1)
  • b. 非保序的元素删除(不常见),时间复杂度为O(1),比如:在指定位置删除(1号位置)删除111,153填进来
  • c. 保序的元素删除,时间复杂度为O(n)

顺序表的不足

顺序表存储时需要连续的内存空间,当要扩充顺序表时会出现以下两种情况:

当空间不足,很有可能无法完成扩容。

链表

不需要连续的存储空间

单链表(单向链表)是链表的一种形式,每个结点包含两个域:元素域和链接域 . 这个链接指向链表中的下一个结点 , 而最后一个结点的链接域则指向一个空值None。

循环单链表,最后一个指向第一个。双向循环依旧如此。

  1. 表元素域item用来存放具体的数据
  2. 链接域next用来存放下一个结点的位置
  3. 变量head指向链表的头结点(首结点)的位置,从head出发能找到表中的任意结点

手搓单向链表

py 复制代码
# 定义节点类
class SingleNode:
    def __init__(self, item):
        self.item = item  # 数据域
        self.next = None  # 链接域


# 定义链表类
class SingleLinkedList:

    def __init__(self, head=None):
        self.head = head  # 头结点

    def is_empty(self):
        return self.head is None

    def length(self):
        cur = self.head
        count = 0
        while cur is not None:
            count += 1
            cur = cur.next
        return count

    def travel(self):
        cur = self.head
        while cur is not None:
            print(cur.item)
            cur = cur.next

    def add(self, item):
        # 创建新的节点
        new_node = SingleNode(item)
        # 将新节点的链接域指向头结点
        new_node.next = self.head
        # 将头结点指向新节点
        self.head = new_node

    def append(self, item):
        # 创建新的节点
        new_node = SingleNode(item)
        if self.is_empty():
            # 链表为空
            self.head = new_node
        else:
            cur = self.head
            while cur.next is not None:
                cur = cur.next
            cur.next = new_node

    def insert(self, pos, item):
        # 插入位置小于等于0,则插入头部
        if pos <= 0:
            self.add(item)
            return
        # 插入位置大于单链表长度,则插入尾部
        if pos >= self.length():
            self.append(item)
            return
        # 插入位置在单链表长度中间
        cur = self.head
        count = 0
        # 查找到要插入的位置
        while count < pos - 1:
            count += 1
            cur = cur.next
        # 此时,cur指向要插入位置的前一个节点
        new_node = SingleNode(item)
        # 注意赋值问题,左侧不会取结果
        new_node.next = cur.next
        cur.next = new_node

    def remove(self, item):
        cur = self.head
        pre = None
        while cur is not None:
            # 找到要删除的节点
            if cur.item == item:
                # 判断是否是头结点
                if cur == self.head:
                    self.head = cur.next
                else:
                    pre.next = cur.next
                break
            else:
                # 走到这,说明不是要删除的节点
                pre = cur
                cur = cur.next

    def search(self, item):
        cur = self.head
        while cur is not None:
            if cur.item == item:
                return True
            cur = cur.next
        return False


if __name__ == '__main__':
    node = SingleNode('乔峰')
    print(node)
    print(node.item)  # 打印数据域
    print(node.next)  # 打印链接域

    linked_list = SingleLinkedList(node)
    print(linked_list.head)  # 头结点是空的

    # 测试获取长度
    print(linked_list.length())

    linked_list.add('令狐冲')

    linked_list.append('张三')
    linked_list.insert(2, '李四')

    linked_list.travel()

    linked_list.remove('张三')
    print('*' * 20)
    linked_list.travel()

    print(linked_list.search('李四'))

顺序表和链表的区别

操作 链表 顺序表
访问元素 O(n) O(1)
在头部插入 / 删除 O(1) O(n)
在尾部插入 / 删除 O(n) O(1)
在中间插入 / 删除 O(n) O(n)

排序

什么是稳定性算法?

具有相同关键字的纪录经过排序后,相对位置保持不变,这样的算法是稳定性算法

  • 不稳定的排序算法: 选择排序、快速排序、希尔排序、堆排序
  • 稳定的排序算法: 冒泡排序、插入排序、归并排序和基数排序

冒泡排序

原理:相邻元素两两比较,大的往后走,每轮结束后,最大的就在最后面。重复直至完成。

核心:

  1. 比较的总轮数
  2. 每轮比较的次数
  3. 谁和谁比较

第一版

py 复制代码
def bubble_sort(my_list):
    # 1. 获取列表的长度
    n = len(my_list)
    # 2. 定义外循环,外循环控制,比较的轮数,内循环控制,比较的次数
    for i in range(n - 1): # i 的值,0,1,2,3
        for j in range(n - 1 - i): # j 的值,0123,012,01,0
            # 比较
            if my_list[j] > my_list[j + 1]:
                # 交换
                my_list[j], my_list[j + 1] = my_list[j + 1], my_list[j]


if __name__ == '__main__':
    l = [5, 3, 4, 7, 2]
    print(f'列表排序前的结果是:{l}')
    bubble_sort(l)
    print(f'列表排序后的结果是:{l}')

冒泡算法时间复杂度

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

遍历一遍发现没有任何元素发生了位置交换,终止排序

算法稳定性 : 稳定算法

优化

减少了时间复杂度

py 复制代码
def bubble_sort(my_list):
    n = len(my_list)
    for i in range(n - 1):
        # 定义变量,记录每轮交换次数
        count = 0
        for j in range(n - 1 - i):
            if my_list[j] > my_list[j + 1]:
                # 交换+1
                count += 1
                my_list[j], my_list[j + 1] = my_list[j + 1], my_list[j]
        # 如果没有发生交换,则提前结束
        if count == 0:
            break

if __name__ == '__main__':
    l = [0, 3, 4, 7, 2]
    print(f'列表排序前的结果是:{l}')
    bubble_sort(l)
    print(f'列表排序后的结果是:{l}')

选择排序

原理:假设第1个元素为最小值,定义变量 min_index 用于记录剩下元素中,最小值的索引,第一轮比较完毕后,如果min_index 的位置发生改变,交换变量即可,最终最小值在最小索引处。

核心:

  1. 比较的总轮数:列表的长度 - 1
  2. 每轮的比较总次数:i + 1 ~ 列表的长度 - 1
  3. 谁和谁比较:min_index 和 j 索引的元素
py 复制代码
def select_sort(my_list):
    n = len(my_list)
    for i in range(n - 1):
        # 假设最小值为第一个元素
        min_index = i
        for j in range(i + 1, n):
            if my_list[j] < my_list[min_index]:
                min_index = j

        if min_index != i:
            my_list[i], my_list[min_index] = my_list[min_index], my_list[i]


if __name__ == '__main__':
    l = [0, 3, 4, 7, 2]
    print(f'列表排序前的结果是:{l}')
    select_sort(l)
    print(f'列表排序后的结果是:{l}')

选择算法时间复杂度

  • 最差时间复杂度 : O(n2)
  • 最优时间复杂度 : O(n2)
  • 算法稳定性: 不稳定算法

插入排序

有2个序列,有序序列和无序序列, 将无序序列中的每一个元素插入到有序序列中

原理:把列表分成有序和无序两部分,每次从无序数据中拿到第一个元素,然后放到对应的有序列表中。

  1. 比较的总轮数:列表长度 - 1
  2. 每轮比较的总次数:i ~ 0
  3. 谁和谁比较:索引 j 和 j - 1的元素比较

属于稳定算法,元素相同的情况下。是不会更换位置的。

  • 最差时间复杂度 : O(n2)
  • 最优时间复杂度 : O(n)
py 复制代码
def select_sort(my_list):
    n = len(my_list)
    for i in range(1, n):
        for j in range(i, 0, -1):
            if my_list[j] < my_list[j - 1]:
                my_list[j - 1], my_list[j] = my_list[j], my_list[j - 1]

if __name__ == '__main__':
    l = [0, 3, 4, 7, 2]
    print(f'列表排序前的结果是:{l}')
    select_sort(l)
    print(f'列表排序后的结果是:{l}')

二分查找(递归版)

原理: 二分查找也叫折半查找,是一种效率比较高的检索算法。

前提:数据必须是有序的,升序,降序均可。

原理:假设数据是升序的。

  1. 获取中间索引的值,进行比较
  2. 如果相等,直接返回True
  3. 如果查找的值比索引的值小,就去中值左,进行查找
  4. 如果查找的值比索引的值大。就去中值右,进行查找
py 复制代码
def binary_search(my_list, item):
    # 1. 获取列表的长度
    n = len(my_list)
    # 2. 如果列表为空,则返回False
    if n == 0:
        return False
    # 3. 获取中间索引
    mid = n // 2
    # 4. 如果中间的元素等于要查找的元素,则返回True
    if my_list[mid] == item:
        return True
    # 5. 如果中间的元素大于要查找的元素,则将列表的前半部分作为新的列表进行递归查找
    if my_list[mid] > item:
        return binary_search(my_list[:mid], item)
    if my_list[mid] < item:
        return binary_search(my_list[mid + 1:], item)
    return False

if __name__ == '__main__':
    l = [2, 3, 5, 9, 13, 27, 31, 39, 55]
    print(binary_search(l, 22))

二分查找(非递归版本)

py 复制代码
def binary_search(my_list, item):
    # 1 设置初始化搜素空间 起始位置start 结束位置end
    start = 0
    end = len(my_list) - 1
    # 2 循环检索
    # 2-1 获取中间值索引 mid = (start + end) // 2
    # 2-2 item等于中间值 返回True
    # 2-3 item小于中间值 在前半部空间搜索 修改搜索空间 end /mid - 1
    # 2-4 item大于中间值 在后半部空间搜索 修改搜索空间 start /mid + 1
    while start <= end:
        mid = (start + end) // 2
        if item == my_list[mid]:
            return True
        if item < my_list[mid]:
            end = mid - 1
        elif item > my_list[mid]:
            start = mid + 1
    return False


if __name__ == '__main__':
    l = [2, 3, 5, 9, 13, 27, 31, 39, 55]
    print(binary_search(l, 31))

树(英语:tree)就是一种非线性结构

它是用来模拟具有树状结构性质的数据集合. 它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做"树"是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的. 它具有以下的特点:

  1. 每个节点有零个或多个子节点
  2. 没有父节点的节点称为根节点
  3. 每一个非根节点有且只有一个父节点
  4. 除了根节点外,每个子节点可以分为多个不相交的子树

树的术语

  • 节点的度:一个节点下的所有子节点个数,不包含孙子节点。
  • 树的度:最大的节点,下的度称为树的度,例如,B节点有三个度,那么树就是三个度。
  • 叶节点或终端节点:度为0的节点
  • 父节点:不是叶节点的节点是父节点
  • 兄弟节点:节点下的两个节点是兄弟节点
  • 堂兄弟节点:同一层的节点是堂兄弟节点
  • 节点层次:从根节点开始是一层以此类推
  • 树的高度和深度:总共几层,节点的最大层次。
  • 节点的祖先:从根到该节点所经分支上的所有节点
  • 子孙:以某节点为根的子树中任一节点都称为该节点的子孙
  • 森林:由m(m>=0)棵互不相交的树的集合称为森林,比如windows 磁盘CDEF

树的种类及存储

树的种类
  • 无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树

  • 有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树

  • 有序树:

霍夫曼树(用于信息编码):带权路径最短的二叉树称为哈夫曼树或最优二叉树

B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多于两个的子树

二叉树:每个节点最多含有两个子树的树称为二叉树

二叉树的种类

完全二叉树 :对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列 ,这样的二叉树被称为完全二叉树,其中满二叉树的定义是所有叶节点都在最底层的完全二叉树

平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树 为什么需要平衡二叉树:防止树退化为链表

排序二叉树(二叉查找树(英语:Binary Search Tree),也称二叉搜索树、有序二叉树)

排序二叉树(BST)的要求:

  1. 若左子树不空,则左子树上所有节点的值均小于它的根节点的值
  2. 若右子树不空,则右子树上所有节点的值均大于它的根节点的值
  3. 左、右子树也分别为二叉排序树

排序二叉树包含空树

注1:一般称为二叉排序树

注2:中序遍历排序二叉树,会得到一个有序的序列

思考

1、二叉树可由很多类型:满二叉树、平衡二叉树、排序二叉树各自作用?

  • 满二叉树:层次存储的时候,从左到右控制节点产生(依次添加,子节点)
  • 平衡二叉树:防止树变成链表
  • 排序二叉树:对数据排序,检索起来速度快。比如查找4

二叉树的存储

顺序存储 :将二叉树存储在固定的数组中,虽然在遍历速度上有一定的优势,但因所占空间比较大,是非主流二叉树 存储方式.二叉树通常以链式存储

思考: 存储树,要存储树的哪些信息?

  1. 树结点数据
  2. 树结点的关系

链式存储 :由于对节点的个数无法掌握,常见树的存储表示转换成二叉树进行处理,子节点个数最多为2

思考:链式存储、顺序存储优缺点?

  1. 链式存储:很容易找到子节点、父节点关系
  2. 顺序存储:直接存数据,没有其他冗余信息,节省空间
总结

二叉树: 每个节点最多含有两个子树的树称为二叉树

完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外 其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续 地紧密排列,这样的二叉树被称为完全二叉树

二叉树的存储方式: 链式存储(每个结点有两个指针域)

分类

  • 完全二叉树:除了最后一层,其他层都是满的
  • 满二叉树:树的所有叶子结点,都在最后一层
  • 平衡二叉树:任意节点的两个子树的高度差不超过一,目的防止树退化成链表
  • 排序二叉树:按照中序(左根右)的顺序获取数据,是一个有序的序列

存储

顺序表:

  • 只存储节点的数据,相对节省空间,有索引,方便查找数据,但是不存储节点之间的关系

链式存储:

  • 不仅存储节点的数据,还存储节点的关系,相对更占用空间
  • 但是方便我们维护和管理节点的关系
  • 推荐把树转成二叉树,每个二叉树都有:data(存数据) + next(左子树,left child) + next(右子树+right child)组成

树的应用场景

  1. xml,html等,那么编写这些东西的解析器的时候,不可避免用到树
  2. 路由协议就是使用了树的算法
  3. mysql数据库索引
  4. 文件系统的目录结构
  5. 所以很多经典的AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构

二叉树的概念和性质

二叉树是每个节点最多有两个子树的树结构 通常子树被称作"左子树"(left subtree)和"右子树"(right subtree)

  • 性质1: 在二叉树的第i层上至多有 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i − 1 2^{i-1} </math>2i−1个结点(i>0) eg:第3层最多结点个数 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 3 − 1 2^{3-1} </math>23−1
  • 性质2: 深度为 k 的二叉树至多有 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 k − 1 2^{k - 1} </math>2k−1 个结点(k>0) eg: 层次 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 3 2^{3} </math>23 1= 7
  • 性质3: 对于任意一棵二叉树,如果其叶结点数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> N 0 N_0 </math>N0 ,而度数为 2 的结点总数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> N 2 N_2 </math>N2,则 <math xmlns="http://www.w3.org/1998/Math/MathML"> N 0 = N 2 + 1 N_0=N_2+1 </math>N0=N2+1
  • 性质4: 最多有 n 个结点的完全二叉树的深度必为 <math xmlns="http://www.w3.org/1998/Math/MathML"> l o g 2 ( n + 1 ) log_2(n+1) </math>log2(n+1)
  • 性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为 i 的结点,其左孩子编号必为 2i,其右孩子编号必为 2i+1 , 其父节点的编号必为 i//2(i=1 时为根,除外)

广度和深度

深度:一路往下走到最深,再回头换一条路继续走。

优点:

  1. 可以快速定位到终点

广度:按层一层一层访问,从上到下,从左到右。

优点:

  1. 帮助定位,找最短路径

代码实现

py 复制代码
# 1. 定义节点类 Node
class Node:
    def __init__(self, item):
        self.item = item
        self.lchild = None
        self.rchild = None


# 2. 定义二叉树类 BinaryTree
class BinaryTree:
    def __init__(self, root=None):
        self.root = root

    # 定义函数,往二叉树中添加元素
    def add(self, item):
        if self.root is None:
            self.root = Node(item)
            return
        # 创建一个队列
        queue = [self.root]
        while queue:
            # 弹出队列的第一个元素
            cur = queue.pop(0)
            # 判断当前节点的左子树
            if cur.lchild is None:
                cur.lchild = Node(item)
                return
            queue.append(cur.lchild)
            # 判断当前节点的右子树
            if cur.rchild is None:
                cur.rchild = Node(item)
                return
            queue.append(cur.rchild)

    # 定义函数,实现广度逐层遍历
    def breadth_travel(self):
        if self.root is None:
            return
        queue = [self.root]
        while queue:
            cur = queue.pop(0)
            print(cur.item)
            if cur.lchild is not None:
                queue.append(cur.lchild)
            if cur.rchild is not None:
                queue.append(cur.rchild)


if __name__ == '__main__':
    bt = BinaryTree()
    bt.add('a')
    bt.add('b')
    bt.add('c')
    bt.add('d')
    bt.add('e')
    bt.add('f')
    bt.add('g')
    bt.add('h')
    bt.add('i')
    bt.breadth_travel()

二叉树的三种深度优先遍历

py 复制代码
# 1. 定义节点类 Node
class Node:
    def __init__(self, item):
        self.item = item
        self.lchild = None
        self.rchild = None


# 2. 定义二叉树类 BinaryTree
class BinaryTree:
    def __init__(self, root=None):
        self.root = root

    # 定义函数,往二叉树中添加元素
    def add(self, item):
        if self.root is None:
            self.root = Node(item)
            return
        # 创建一个队列
        queue = [self.root]
        while queue:
            # 弹出队列的第一个元素
            cur = queue.pop(0)
            # 判断当前节点的左子树
            if cur.lchild is None:
                cur.lchild = Node(item)
                return
            queue.append(cur.lchild)
            # 判断当前节点的右子树
            if cur.rchild is None:
                cur.rchild = Node(item)
                return
            queue.append(cur.rchild)

    # 定义函数,实现广度逐层遍历
    def breadth_travel(self):
        if self.root is None:
            return
        queue = [self.root]
        while queue:
            cur = queue.pop(0)
            print(cur.item)
            if cur.lchild is not None:
                queue.append(cur.lchild)
            if cur.rchild is not None:
                queue.append(cur.rchild)

    # 先序遍历
    def preorder(self, node):
        if node is not None:
            # 访问根节点
            print(node.item, end=' ')
            # 访问左子树
            self.preorder(node.lchild)
            # 访问右子树
            self.preorder(node.rchild)
        # 结果 0137849256

    # 中序遍历
    def inorder(self, node):
        if node is not None:
            # 访问左子树
            self.inorder(node.lchild)
            # 访问根节点
            print(node.item, end=' ')
            # 访问右子树
            self.inorder(node.rchild)
        # 7 3 8 1 9 4 0 5 2 6 

    # 后序遍历
    def postorder(self, node):
        if node is not None:
            # 访问左子树
            self.postorder(node.lchild)
            # 访问右子树
            self.postorder(node.rchild)
            # 访问根节点
            print(node.item, end=' ')
        
        # 7 8 3 9 4 1 5 6 2 0 

if __name__ == '__main__':
    bt = BinaryTree()
    bt.add(0)
    bt.add(1)
    bt.add(2)
    bt.add(3)
    bt.add(4)
    bt.add(5)
    bt.add(6)
    bt.add(7)
    bt.add(8)
    bt.add(9)
    bt.preorder(bt.root)
    print()
    bt.inorder(bt.root)
    print()
    bt.postorder(bt.root)

二叉树由遍历结果反推二叉树的结构

  • 先序遍历: 0 1 3 7 8 4 9 2 5 6: 根 左 右
  • 中序遍历: 7 3 8 1 9 4 0 5 2 6: 左 根 右
  • 后序遍历: 7 8 3 9 4 1 5 6 2 0: 左 右 根

知道中序遍历 和 先序遍历 或者 后序遍历 就可以推出二叉树的结构

思路:

通过先序遍历可以确定哪个元素是根节点,通过中序遍历可以知道左子树都有那些结点、右子树都有那些结点。

  1. 有了树(先序、中序表示),根据先序确认根节点,中序确定左子树,右子树;有了左子树和右子树,相当于2颗树
  2. 重复步骤1;直到划分完毕
相关推荐
永远有缘6 小时前
四种编程语言常用函数对比表
java·开发语言·c++·python
Pocker_Spades_A6 小时前
Python快速入门专业版(五十三):Python程序调试进阶:PyCharm调试工具(可视化断点与变量监控)
开发语言·python·pycharm
stayhungry_c7 小时前
Quarto生成PDF无法正常显示中文的问题
python·macos·r语言·pdf
程序员小远7 小时前
selenium元素定位---(元素点击交互异常)解决方法
自动化测试·软件测试·python·selenium·测试工具·测试用例·交互
ColderYY7 小时前
DrissionPage自动化
python·自动化
Python大数据分析@7 小时前
如何用 Python xlwings库自动化操作 Excel?
python·自动化·excel
qq_12498707537 小时前
基于Flask的穷游网酒店数据分析系统(源码+论文+部署+安装)
后端·python·flask·毕业设计
Brianna Home7 小时前
PyTorch实战:CV模型搭建全指南
人工智能·pytorch·经验分享·python·神经网络·结对编程
喜欢吃豆8 小时前
spec-kit深度解析:AI驱动的规范驱动开发(SDD)的架构、哲学与实践启示
人工智能·驱动开发·python·架构·大模型