线性表

线性表的概念和表抽象数据类型

表的概念和性质

  • 线性表

    一个(有穷或无穷)的基本元素集合 E, E 中一组有穷个元素排列成的序列 L=(e0,e1,e2,,en1)

  • 下标

  • 空表

  • 长度

  • 首元素 & 尾元素

    唯一

  • 前驱元素

  • 后继元素

表抽象数据类型

线性表的操作

表抽象数据类型

线性表的实现:基本考虑

  1. 空间(计算机内存)
  2. 时间(各种重要操作的效率)

顺序表的实现

顺序表——表中元素顺序存放在一片足够大的连续存储区内,首元素存入存储区开始位置,其余元素依次顺序存放,元素之间的逻辑顺序关系通过元素在存储区域里的物理位置表示。

基本实现方式

若顺序表中存储的元素类型相同,则:

  • 存取操作可以在 O(1) 的时间内完成。
  • 元素访问是 O(1) 复杂度的操作

若顺序表中元素大小不统一,则

  • 可以将实际数据元素另行存储,在顺序表里各单元位置保存相应元素的引用信息(链接/索引)。

顺序表基本操作的实现

创建和访问操作

  • 创建空表`

    创建新表的存储区后,应立即将两个表信息域(max 与 num) 设置好,保证这个表处于合法状态

  • 简单判断操作 O(1)

    • 空表:num == 0
    • 表满:num == max
  • 访问给定下标 i 的元素 O(1)

  • 遍历操作 O(n)

  • 查找给定元素 d 的(第一次出现的)位置 O(n)

  • 查找给定元素 d 在位置 k 后第一次出现的位置 O(n)

变动操作:加入元素

  • 尾端加入新数据项 O(1)

  • 新数据存入元素存储区的第 i 个单元

    • 不要求维持原有元素的相对位置:O(1)

      将原有第 i 个单元的元素放入 num,再将新元素写入第 i 个单元

    • 要求保持原有元素的相对位置: O(n)

      把包括第 i 个单元的所有单元向后平移一位,再将新元素写入第 i 个单元

变动操作:删除元素

  • 尾端删除数据 O(1)

  • 删除位置 i 的数据

    • 软删除

      添加合法下标,再删除元素时将合法下标改为非法

    • 硬删除

      • 不需要保持原有顺序 O(1)

        将 num - 1 填入 i,再尾端删除

      • 需要保持原有顺序 O(n)

        将第 i 个删除,再将其后面的每个元素前移一位

  • 基于条件的删除 O(n)

    基于条件:不直接给出删除元素的位置,而是给出需要删除数据项的条件

    需要通过循环实现,循环中逐个检查元素,查找到后将其删除。

顺序表及其操作的性质

顺序表优缺点总结:

  • 优点
    • O(1) 时间的(随机、直接的)按位置访问元素;
    • 元素在表里存储紧凑,除表中的存储区之外只需要 O(1) 空间存放少量的辅助信息
  • 缺点
    • 需要连续的存储区存放表中的元素,若表很大,则需要大片连续的内存空间。
    • 一旦确定了存储块的大小,可容纳单元个数并不随着插入 /删除操作的进行而变化。如果很大的存储区只保存了少量的数据项,就会有大量空闲单元,造成表内的存储浪费。
    • 另外,在执行加入或删除操作时,通常需要移动许多元素,效率低。
    • 最后,建立表需要考虑元素存储区大小,而实际需求通常很难事先估计。

Python 的 list

  • 创建线性表

  • lst = []

  • 加入元素

    • 尾端加入单一元素:lst.append(x)
    • 将数据存入第 i 个单元:lst.insert(i, x)
  • 删除元素

    • 删除尾端元素:lst.pop()
    • 删除下标为 i 的元素:lst.pop(i)
    • 删除第一个内容为 x 的元素:lst.remove(x)
  • 求表长

    • len(lst)
  • 清除 list 中的所有元素

    • lst.clear()
  • 将 list 中的元素倒置

    • lst.reverse()
  • 将 list 中的元素排序

    • sort:会修改 list 本身,不会返回新的 list
    • sorted:不会修改 list 本身,会返回排序好的 list
    lst = [3,4,5,1,2]
    print(lst.sort())   # None
    print(lst)          # [1,2,3,4,5]
    ######
    lst = [3,4,5,1,2] 
    print(sorted(lst))  # [1,2,3,4,5]
    print(lst)          # [3,4,5,1,2]
    

链接表(链表)

线性表的基本需要和链接表

链接表的基本思路:

  • 把表中的元素分别存储在一批独立的存储块(称为表的结点)里
  • 保证从组成表结构中的任一个节点可找到于其相关的下一个结点
  • 在前一结点里用链接的方式显式地记录与下一个结点的关联

单向链接表(单链表)

image-20210103164334335

image-20210103164051115

  • 一个单链表由一些具体的表结点构成
  • 每个结点是一个对象,由自己的标识,下面也常称其为该节点的链接
  • 结点之间通过结点链接建立其单向顺序联系
# 定义一个简单的表结点类
class LNode:
    def __init__(self, elem, next_ = None):
        self.elem = elem
        self.next = next_ # 为了避免与 python 标准函数 next 重名

基本链表操作

创建空链表

把相应的表头变量设置为空连接

删除链表

将表指针赋值为 None

判断表是否为空

检查表头指针是否为 None

判断表是否满

一般链表不会满

加入元素

表首端插入

  1. 创建一个新结点并存入数据
  2. 把原链表首结点的链接存入新结点的链接域 next
  3. 修改表头变量,使之指向新结点
q = LNode(13)
q.next = head
head = q

一般情况的元素插入

  1. 创建一个新结点并存入数据
  2. pre 所指结点的 next 域的值存入新结点的链接域 next
  3. 修改 prenext 域,使之指向新结点

image-20210103170529842

q = LNode(13)
q.next = pre.next
pre.next = q

删除元素

删除表首元素

修改表头指针,令其指向表中的第二个结点

head = head.next

一般情况的元素删除

image-20210103170904240

pre.next = pre.next.next

扫描、定位和遍历

p = head
while p is not None and 还需要继续的其它条件:
     p 所指结点里的数据做所需操作
    p = p.next

按下标定位

p = head
while p is not None and i > 0:
    i -= 1
    p = p.next

按元素定位

p = head
while p is not None and not pred(p.elem):
    p = p.next

遍历

p = head
while p is not None:
    p = p.next

链表操作的复杂度

操作 具体说明 复杂度
创建空表   O(1)
删除表   O(1)
判断空表   O(1)
加入元素 首端加入元素 O(1)
  尾端加入元素 O(n)
  定位加入元素 O(n)
删除元素 首端删除元素 O(1)
  尾端删除元素 O(n)
  定位删除元素 O(n)
  其它删除:通常需要扫描一整个表或者一部分 O(n)

求表的长度 O(n)

def length(head):
    p, n = head, 0
    while p is not None:
        n += 1
        p = p.next
    return n

链表的变形和操作

单链表的简单变形

在表对象中加入一个表尾引用域

循环单链表(循环链表)

最后一个结点的 next 域不用 None,而是指向表的第一个结点。

双向链接表(双链表)

节点之间由双向链接: prev, next

循环双链表

链表的排序

排序操作见后文“内部排序”

栈和队列

概述

栈、队列和数据使用顺序

栈 Stack

栈是保证元素后进先出(后存入先使用, Lat In First Out, LIFO) 关系的结构,简称 LIFO 结构。

队列 Queue

队列是保证元素先进先出(先存入者先使用,First In First Out, FIFO)关系的结构,简称 FIFO 结构。

应用环境

  • 计算过程分为一些顺序执行的步骤
  • 计算中执行的某些步骤会不断产生一些后面可能需要的中间数据
  • 产生的数据中有些不能立即使用,但又需要在将来使用
  • 需要保存的数据项数不能事先确定

栈:概念和实现

栈抽象数据类型

栈的线性表实现

  • 对于顺序表后端插入和删除都是 O(1) 操作,应该用这一端作为栈项(采用顺序表实现)
  • 对于连接表前端插入和删除都是 O(1) 操作,应该用这端作为栈项

栈的顺序表实现

采用 Python 的 list 数据结构

  • 建立空栈:lst = []
  • 压栈:lst.append()
  • 弹栈:lst.pop()

栈的连接表实现

见前文连接表

栈的应用

数值转换

def Conversion(n, d): # 十进制数 n 转化为 d 进制数
    st = []
    while n > 0:
        st.append(n % d)
        n //= d
    while not st == []:
        print(st.pop(), end = "")
    print('')
    
# 试着运行一下
Conversion(8,2)

括号匹配问题

处理思路

  • 顺序扫描被检查正文(一个字符串)中的每一个字符
  • 检查中跳过无关字符(非括号字符)
  • 遇到开括号”(“时将其压入栈
  • 遇到闭括号时弹出当前的栈顶元素与之匹配
  • 如果匹配成功则继续,发现不匹配时检查以失败结束

具体实现

def check_parens(text):
    """括号匹配检查函数,text 是被检查的正文串"""
    parens = "()[]{}" # 所有括号字符
    open_parens = "([{" # 开括号字符
    opposite = {")":"(", "]":"[", "}":"{"} # 表示匹配关系的字典
    
    def parentheses(text):
        """括号生成器,每次调用返回 text 里下一个括号及其位置"""
        i, text_len = 0, len(text)
        while True:
            while i < text_len and text[i] not in parens:
                i += 1
            if i >= text_len:
                return 
            yield text[i], i
            i += 1
            
    st = [] # 保存括号的栈
    for pr, i in parentheses(text): # 对 text 里各括号和位置迭代
        if pr in open_parens: # 开括号,压栈并继续
            st.append(pr)
        elif st == []: # 无法再弹栈
            return False
        elif st.pop() != opposite[pr]: # 不匹配就是失败,退出
            return False
        # else: 这是依次成功匹配,什么也不做,继续,因为上一步已经弹栈了
    if st == []:
        return True
    else:
        return False

# 应用一下试试看
re = check_parens("{(]")
if re:
    print("All parentheses are correctly matched.")
else:
    print("Parentheses mismatched.")

表达式的表示,计算和变换

表达式和计算的描述

  • 中缀形式:(3 - 5) * (6 + 17 * 4) / 3

    中缀表达式表达能力最弱,只有在添加括号后才可达到相同的表达能力

  • 前缀形式:/ * - 3 5 + 6 * 17 4 3

  • 后缀形式:3 5 - 6 17 4 * + * 3 /

后缀表达式的计算

算法思路
  • 遇到运算对象是,应该记录它以便后续使用
  • 遇到运算符时,应该根据其元数(假定都是二元运算符),取得前面最近遇到的几个运算对象或已完成运算的结果,应用这个运算符计算,并保存其结果
注意事项
  • 需要记录的是已经掌握的数据
  • 每次处理运算符,应使用的是此前最后记录的几个结果
# 实现的伪代码如下:
# 假定 st 是一个栈,算法的核心是下面的循环
while 还有输入:
    x = nextItem() # 获取下一个输入
    if is_opend(x): # 如果是运算对象
        st.append(float(x))
    else: # 如果是运算符
        b = st.pop() # 第二个运算对象 ################################## 特别注意,先弹出来的是第二个运算数
        a = st.pop() # 第一个运算对象 ################################## 特别注意,后弹出来的是第一个运算数
        根据运算符对 a, b 进行运算
        st.append(c) #计算结果压入栈
def calculate(formula):
    formula = formula.split()
    st = [] # 栈
    for i in formula:
        if(i.isdigit()): # 若为数字
            st.append(int(i)) # 只支持整形运算
        else:
            b = st.pop() # 第二个运算数 ################################## 特别注意,先弹出来的是第二个运算数
            a = st.pop() # 第一个运算数 ################################## 特别注意,后弹出来的是第一个运算数
            if(i == '+'):
                c = a + b
            elif(i == '-'):
                c = a - b
            elif(i == '*'):
                c = a * b
            elif(i == '/'):
                c = a / b
            else:
                print("Illegal operator")
                return
            st.append(c)
    return st.pop()

# 试着运算看看
print(calculate("3 5 - 6 17 4 * + * 3 /"))

中缀表达式到后缀表达式的转换

  • 扫描中遇到一个运算符不能将其输出,只要看到下一个运算符的优先级不高于本运算符的时候,才能够取做本运算符要求的计算
  • 应该用一个栈保存尚未处理的运算符
  • 需要处理括号问题
  • 在扫描完成后,栈里可能剩下一些运算符,应将其一一弹出并送到后缀表达式。

中缀表达式的求值

栈与递归

阶乘函数的递归计算

栈与递归 / 函数调用

为了支持递归定义函数的实现,需要一个栈(运行栈)保存递归函数执行时每层调用的局部信息

队列

队列抽象数据类型

先进先出(First In First Out, FIFO)结构

  • 入队(enqueue)
  • 出队(dequeue)

队列的链接表实现

需要使用尾端指针

队列的顺序表实现

  • 初始化 front = rear = 0
  • 入队:将新元素插入 rear 所指向的位置,然后 rear 加一
  • 出队:删去 front 所指的元素,然后加 1 并返回被删元素
  • 队列为空:front = rear
  • 队满:rear = MAX_QUEUE_SIZE - 1 或 front = rear

基于顺序表实现队列的困难

假溢出

在入队和出队操作中,头、尾指针只增加不减小,致使被删除元素的空间永远无法重新利用。因此,尽管队列中实际元素个数可能远远小于数组大小,但可能由于尾指针巳超出向量空间的上界而不能做入队操作。

循环顺序表(循环队列)

为循环队列所分配的空间可以被充分利用,除非向量空间真的被队列元素全部占用,否则不会上溢。

  • q.elems —— 始终指向表元素区开始
  • q.head —— 对头变量,记录当前队列里第一个元素的位置
  • q.rear —— 队尾变量,记录当前队列里最后元素之后的第一个空位

image-20210103210448289

image-20210103210459953

入队和出队分别需要更新变量 q.headq.rear

  • q.head = (q.head + 1) % q.len
  • q.rear = (q.rear + 1) % q.len

队列的应用

BFS

字符集、字符串和字符串操作

字符串的相关概念

  • 字符串长度

  • 字符在字符串中的位置

  • 字符串相等

  • 字典序

  • 字符串拼接

  • 子串关系

    任何字符串也是该串自身的子串

  • 前缀后缀

字符串抽象数据类型

字符串的实现

基本实现问题和技术

实际语言里的字符串

Python 里的字符串

str 的操作

  • 切分操作 split

    string.split('x')

    • 默认在空格处切分
    • 返回切分后的 list,其中每个元素不包含 ‘x’
    • 不改变原有字符串
  • 替换操作 replace

    string.replace('a', 'b')

    • 一次替换掉全部满足要求的元素
    • 返回替换后的结果
    • 不改变原有字符串
  • 检查子串出现的次数 count

    TimesOccured = string.count('x', start(, end))

  • 检查后缀 endswith

    string.endswith('Ending')

    • 返回 TureFalse
  • 找子串的位置 find/index

    string.find("substring", start(, end))

    string.index("substring", start(, end))

    • index() 方法检测字符串中是否包含子字符串 str ,该方法与 python find()方法一样,只不过如果 str 不在 string会报一个异常

字符串匹配(子串查找)string matching

字符串匹配

假设有两个串(其中 ti,pi 是字符)

t=t0t1t2tn1

p=p0p1p2pm1

t 中查找与 p 相同的子串

  • 目标串t
  • 模式串p

通常有 m « n, 即模式串长度远小于目标串长度

实际的串匹配问题

串匹配和朴素匹配算法

串匹配算法

朴素的串匹配算法 Brute Force

acdsgsdshvdncxmcudiwdnskxjzxjkxnvzbcshdiquso
dsh
acdsgsdshvdncxmcudiwdnskxjzxjkxnvzbcshdiquso
 dsh
acdsgsdshvdncxmcudiwdnskxjzxjkxnvzbcshdiquso
  dsh
acdsgsdshvdncxmcudiwdnskxjzxjkxnvzbcshdiquso
   dsh
acdsgsdshvdncxmcudiwdnskxjzxjkxnvzbcshdiquso
    dsh
acdsgsdshvdncxmcudiwdnskxjzxjkxnvzbcshdiquso
     dsh
acdsgsdshvdncxmcudiwdnskxjzxjkxnvzbcshdiquso
      dsh

无回溯串匹配算法(KMP 算法)

image-20210103215911275

基本考虑

问题分析

pi 匹配失败时,所有的 pk(0k<i) 都已经匹配成功 。因此,只需要根据模式串 p 本身即可决定匹配失败时如何前移。

p 中的每个 i,都有与之对应的下标 ki,与之匹配的目标串无关。(ki 课通过对于模式串 p 的预分析得到)假设模式串 p 的长度为 m,则需要对每个 i(0i<m) 计算出对应的 ki 并将其保存起来,以便在匹配中使用。为此可以考虑一个长为 m 的表 pnext,用表元素 pnext[i] 记录与 i 对应的 ki 值。

KMP 算法 O(n)

def matching_KMP(t, p, pnext):
    """KMP 串匹配,主函数"""
    j, i = 0, 0
    n, m = len(t), len(p)
    while j < n and i < m: # i == m 说明找到了匹配
        if i == -1 or t[j] == p[i]: # 考虑 p 中下一个字符
            j, i = j + 1, i + 1
        else: # 失败!考虑 pnext 决定的下一字符
            i = pnext[i]
    if i == m: # 找到匹配,返回其下标
        return j-1
    return -1 # 无匹配,返回特殊值

构造 pnext 表:分析

  • 模式串移动之后,作为下一个用于匹配的字符的新位置,其前缀子串应该与匹配失败的字符串之前同样长度的子串相同。
  • 如果匹配在模式串的位置 i 失败时,二位置 i 的前缀子串中满足上述条件的位置不止一处,那么只可能做最短的移动,将模式串移到最近的那个满足上述条件的位置,以保证不遗漏可能的匹配。
  • 如果 p0pi1 的最长相等前后缀的长度为 k(0k<i1),在 pitj 时,模式串就应该右移 ik 位,即应把 pnext[i] 设置为 k

递推计算最长相等前后缀的长度

image-20210104143909921

已知 pnext[0] = -1 和直至 pnext[i-1] 的已有值求 pnext[i] 的算法:

  1. 假设 pnext[i-1] = k-1。如果 pi=pk,那么 p0pi最长相等前后缀的长度就是 k,将其计入 pnext[i],将 i 的值加一后继续递推(循环)
  2. 如果 pipk,就将 k 设置为 pnext[k] 的值
  3. 如果 k 的值等于 -1(这个值一定是第 2 步而来自 pnext),那么 p0pi 的最长相同前后缀的长度就是 0,设置 pnext[i] = 0,将 i 的值加 1 后继续递推。

pnext 生成算法的改进

def gen_pnext(p):
    """生成针对 p 中各位置 i 的下一个检查位置表,用于 KMP 算法,有稍许修改的优化版本"""
    i, k, m = 0, -1, len(p)
    pnext = [-1] * m
    while i < m-1: # 生成下一个 pnext 元素
        if k == -1 or p[i] == p[k]:
            i, k = i + 1, k + 1
            if p[i] == p[k]:
                pnext[i] = pnext[k]
            else:
                pnext[i] = k
        else:
            k = pnext[k]
    return pnext

KMP 算法的时间复杂性及其它

  • 构造 pnext O(m)
  • 实际匹配 O(n)
  • 综上: O(m+n)

数组

数组

数组的定义

数组的抽象数据类型定义

数组的顺序表示和实现

一般采用顺序存储的方法来表示数组

行优先顺序(Row Major Order)

a11 a12  a1n a21 a22  a2n  am1 am2  amn

列优先顺序(Column Major Order)

a11 a21  am1 a12 a22  am2  a1n a2n  amn

不同存储方式的地址计算

矩阵的压缩存储

对于高阶矩阵,若其中非零元素呈某种规律分布或者矩阵中有大量的零元素,则考虑压缩存储

  • 多个相同的非零元素只分配一个存储空间
  • 零元素不分配空间

特殊矩阵

是指非零元素或零元素的分布有一定规律的矩阵。

对称矩阵

对称矩阵中的元素关于主对角线对称,因此,让每一对对称元素aijaji,(ij)分配一个存储空间,则n2个元素压缩存储到n(n+1)2个存储空间,能节约近一半的存储空间。

三角矩阵

三角矩阵中的重复元素c可共享一个存储空间,其余的元素正好有n(n+1)2个,因此,三角矩阵可压缩存储到向量sa[0n(n+1)2]中,其中c存放在向量的第1个分量中。

对角矩阵

矩阵中,除了主对角线和主对角线上或下方若干条对角线上的元素之外,其余元素皆为零。

对角矩阵可按行优先顺序或对角线顺序,将其压缩存储到一个向量中,并且也能找到每个非零元素和向量下标的对应关系。

稀疏矩阵 Sparse Matrix

稀疏矩阵的压缩存储

对于稀疏矩阵,采用压缩存储方法时,只存储非0元素。必须存储非0元素的行下标值、列下标值、元素值。因此,一个三元组(i, j, aij)唯一确定稀疏矩阵的一个非零元素。

三元组顺序表

若以行序为主序,稀疏矩阵中所有非0元素的三元组,就可以得构成该稀疏矩阵的一个三元组顺序表。

求转置矩阵
  1. 将矩阵的行、列下标值交换。即将三元组表中的行、列位置值i 、j相互交换;
  2. 重排三元组表中元素的顺序。即交换后仍然是按行优先顺序排序的。
  • 方法一:

    • 算法思想:

      按稀疏矩阵A的三元组表a.data中的列次序依次找到相应的三元组存入b.data中。

    • 算法分析:

      时间复杂度为O(cn×tn),即矩阵的列数和非0元素的个数的乘积成正比。

  • 方法二:(快速转置)

    • 算法思想:

      直接按照稀疏矩阵A的三元组表a.data的次序依次顺序转换,并将转换后的三元组放置于三元组表b.data的恰当位置。

    • 前提:

      若能预先确定原矩阵A中每一列的(即B中每一行)第一个非0元素在b.data中应有的位置,则在作转置时就可直接放在b.data中恰当的位置。因此,应先求得A中每一列的非0元素个数。

    • 附设两个辅助向量num[ ]cpot[ ]

      • num[col]:统计A中第col列中非0元素的个数;
      • cpot[col] :指示A中第一个非0元素在b.data中的恰当位置。
稀疏矩阵的乘法
  • 算法思想:

    对于A中的每个元素a.data[p](p=1, 2, … , a.tn),找到B中所有满足条件:a.data[p].col=b.data[q].row 的元素b.data[q],求得a.data[p].value * b.data[q].value,该乘积是cij中的一部分。求得所有这样的乘积并累加求和就能得到cij

十字链表

矩阵非零元素结点所含有的域有:行、列、值、行指针(指向同一行的下一个非零元)、列指针(指向同一列的下一个非零元)

image-20210104151844343

由定义知,稀疏矩阵中同一行的非0元素的由right指针域链接成一个行链表, 由down指针域链接成一个列链表。则每个非0元素既是某个行链表中的一个结点,同时又是某个列链表中的一个结点,所有的非0元素构成一个十字交叉的链表。称为十字链表

其次,十字交叉链表还有一个头结点,结点的结构如图所示。

image-20210104152456025

image-20210104154357153

广义表 List

广义表是线性表的推广和扩充

广义表的概念

广义表是由 n(n0) 个元素组成的有穷序列:Lst=(a1,a2,an)

其中 ai 或者是原子项(不可再分),或者是一个广义表

  • 表头

  • 表尾

  • 表深

    括号的最大层数

广义表的存储结构

由于广义表中的数据元素具有不同的结构,通常用链式存储结构表示,每个数据元素用一个结点表示。因此,广义表中就有两类结点:

  • 表结点:

    用来表示广义表项,由标志域,表头指针域,表尾指针域组成

  • 原子结点:

    用来表示原子项,由标志域,原子的值域组成

image-20210104161411826

树形结构是由结点(结构中的逻辑单元,可用于保存数据)和结点之间的连接关系(一种后继关系)构成,其结构域线性结构(表)不同,主要特征有:

  • 一个结构如果不空,其中就存在着唯一的起始点,称为树根(root)
  • 一个结点有且只有一个前驱,可以有 0 个或者多个后继
  • 结构里的所有结点都在树根结点通过后继关系可达的结点集合里
  • 结点之间的联系不会构成循环关系
  • 从任意俩能够不同的结点出发,通过后继关系可达的两个结点的集合,或者互不相交,或者一个为另一个的子集。

二叉树:概念和性质

概念和性质

定义和图示

  • 二叉树
  • 左子树
  • 右子树
  • 空树
  • 单点树
  • 子节点
  • 父节点
  • 树叶
  • 度数——一个结点的子节点个数

路径、结点的层和树的高度

二叉树的性质

满二叉树、扩充二叉树

  • 满二叉树:二叉树中所有分支结点的度数都是 2
  • 扩充二叉树:将二叉树扩充为满二叉树,新增加的结点称为外部结点,原有结点称为内部结点

完全二叉树

对于一棵高为 h 的二叉树,如果其第 0 层到第 h1 层的结点都满,最下一层的结点不满,且所有结点在最左边联系排列,空位都在右边。

image-20210104163949799

抽象数据类型

遍历二叉树

image-20210104165302639

A B C D E F G None H None I J K

深度优先遍历 (DFS)

  • 先根序遍历(DLR) 先根序列

    A B D H E I C F J K G

  • 中根序遍历(LDR),也称对称序 中根序列

    D H B E I A J F K C G

  • 后根序遍历(LRD) 后根序列

    H D I E B J K F G C A

宽度优先遍历 (BFS)

按层次顺序遍历

A B C D E F G H I J K

遍历与搜索

二叉树的 list 实现

设计和实现

  • 空树用 None 表示

  • 非空二叉树用包含桑元素的表[d, l, r] 表示,其中:

    • d 表示存在根节点的元素
    • lr 是两棵子树
  • 表示样例:

    ['A', ['B', None, None],
          ['C', ['D', ['F', None, None],
                      ['G', None, None],
                 'E', ['H', None, None],
                      ['I', None, None]]]]
    

二叉树的 class 实现与遍历

树类型定义

class TreeNode(object):
    def __init__(self, value, left, right):
        self.value = None
        self.left = None
        self.right = None

根据层序输入生成树

def CreateTree(root, tree):
    """根据层序输入生成树"""
    queue = []
    queue.append(root)
    global i
    while(i < len(tree)):
        t = queue[0]
        t.value = tree[i]
        queue.pop(0)
        t.left = TreeNode(None, None, None)
        t.right = TreeNode(None, None, None)
        queue.append(t.left)
        queue.append(t.right)
        i += 1

递归遍历

递归前序遍历

def presearch(root):
    """递归——前序遍历"""
    if not root:
        return None
    else:
        if not(root.value == "None" or root.value == None):
            print(root.value, end = " ")
        presearch(root.left)
        presearch(root.right)

递归中序遍历

def midsearch(root):
    """递归——中序遍历"""
    if not root:
        return None
    else:        
        midsearch(root.left)
        if not(root.value == "None" or root.value == None):
            print(root.value, end = " ")
        midsearch(root.right)

递归后序遍历

def postsearch(root):
    """递归——后序遍历"""
    if not root:
        return None
    else:        
        postsearch(root.left)        
        postsearch(root.right)
        if not(root.value == "None" or root.value == None):
            print(root.value, end = " ")

非递归遍历

非递归前序遍历

代码
def nonrec_presearch(root):
    """非递归——前序遍历"""
    dlr = []
    stk = [] # 栈空间
    now = root
    while not(now == None or now.value == None or now.value == "None") and (len(stk) == 0)):
        elif now == None or now.value == None or now.value == "None":
            now = stk.pop()
        else:
            dlr.append(now.value)
            stk.append(now.right)
            now = now.left  
    # 打印出遍历结果
    print(dlr)
算法思想
while 当前结点不为空 or 栈空间不为空时:
    if 当前结点为空:
        当前结点 = stack.pop()
    else:
        1. 先访问当前结点根节点
        2. 右子节点进栈
        3. 当前结点设置为左子节点

非递归中序遍历

代码
def nonrec_midsearch(root):
    """非递归——中序遍历"""
    ldr = []
    stk = [] # 栈空间
    now = root
    while not ((now.value == None or now.value == "None") and (len(stk) == 0)):
        if now.value == None or now.value == "None":
            now = stk.pop()
            ldr.append(now.value)
            now = now.right
        else:
            stk.append(now)
            now = now.left
    # 打印出遍历结果
    print(ldr)
算法思想
while 当前结点不为空 or 栈空间不为空时:
    if 当前结点为空:
        当前结点 = stack.pop()
        访问当前节点
        当前结点设置为右子节点
    else:
        stack.push(当前节点)
        当前结点设置为左子节点

非递归后序遍历

代码
def nonrec_postsearch(root):
    """非递归——后序遍历"""
    lrd = []
    stk = [] # 栈空间
    now = [root, 0]
    while not ((now[0].value == None or now[0].value == "None" or now[0] == None) and (len(stk) == 0)):
        if now[0].value == None or now[0].value == "None" or now[0] == None:
            now = stk.pop()
            now[1] += 1
        elif now[1] == 0:
            stk.append(now)
            now = [now[0].left,0]
        elif now[1] == 1:
            stk.append(now)
            now = [now[0].right,0]
        elif len(stk) == 0:
            lrd.append(now[0].value)
            break
        else:
            lrd.append(now[0].value)
            now = stk.pop()
            now[1] += 1
    # 打印出遍历结果
    print(lrd)
算法思想
需要引入"计数"即是否可以访问根节点
计数为 0: now = now.left
    1: now = now.right
    2: 访问当前结点

树的存储

树、森林与二叉树的转换

霍夫曼树的构造

设有实数集 W=w0,w1,,wm1, T 是一棵扩充二叉树,其 m 个外部节点分别以 wi(i=1,2,,n1) 为权,且 T 的带权外部路径长度 WPL 在所有这样的扩充二叉树中达到最小,则称 T 为 W 的最优二叉树或哈夫曼树。

概念、性质与实现

定义与图示

  • 有向图
  • 无向图

概念与性质

  • 完全图:任意两个顶点之间都有边的图
  • :一个顶点的度就是与它邻接边的条数。
    • 入度
    • 出度

路径的相关性质

  • 路径的长度:该路径上边的条数
  • 回路
  • 简单回路:一个环路,除起点和终点外其它顶点均不相同
  • 简单路径:内部不包含回路的路径
  • 有根图:在有向图里存在一个顶点 v,到其它每个顶点均有路径

连通图

  • 连通
  • 连通无向图:任意两个顶点之间都连通
  • 连通有向图:任意两个顶点之间都有路径(要求两个方向的路径都存在)

子图、连通子图

  • 极大连通子图
  • 极大强连通子图

带权图和网络

  • 带权图
  • 网络

图抽象数据类型

图的表示和实现

邻接矩阵

A_{ij} = \left{ \begin{array}{lr} 1, 如果顶点 v_i 到 v_j 有边 \ 0,如果顶点 v_i 到 v_j 无边\end{array}\right.

图的邻接表表示

对图的每个顶点建立一个单链表,存储该顶点所有邻接顶点及其相关信息。每一个单链表设一个表头结点。

image-20210105165157110

图的 Python 实现

用字典实现

  • 字典的声明:

    Graph = dict()

  • 使用:

    Graph[key] = value

由输入构造树

# get input
NodeNum, PathNum, Start, End = input().split()
NodeNum = int(NodeNum)
PathNum = int(PathNum)
Start = int(Start)
End = int(End)
# Create Graph
Graph = dict()
for i in range(NodeNum):
    Graph[i+1] = dict()
for i in range(PathNum):
    temp = input().split()
    Graph[int(temp[0])][int(temp[1])] = int(temp[2])

图的遍历

深度优先遍历

宽度优先遍历

生成树

条件:连通无向图强连通有向图

如果图 G 有 n 个顶点,必然可以找到 G 中的一个包含 n-1 条边的集合,这个集合里包含了从 v0 到其它所有点的路径。

遍历和生成树

构造 DFS 生成树
构造 BFS 生成树

最小生成树

最小生成树问题

最小生成树 —— 带权树中权值最小的生成树

Kruskal 算法

算法思路
将图看作离散的点和一堆边
dot = []
while num() < n - 1:
    从未选择的边中找到满足<m ,n>
    1. 权重最小
    2. 两个端点不同时出现在已连接的点中
    dot.append(m)
    dot.append(n)
    将边从未选择中删除
    num() ++
![img](https://upload-images.jianshu.io/upload_images/3755117-2656ffcd5cdb097d.png?imageMogr2/auto-orient/strip imageView2/2/w/1200/format/webp)
![img](https://upload-images.jianshu.io/upload_images/3755117-8392698e3388fece.png?imageMogr2/auto-orient/strip imageView2/2/w/1200/format/webp)

Prim 算法

![img](https://upload-images.jianshu.io/upload_images/3755117-4491cf0d977af08c.png?imageMogr2/auto-orient/strip imageView2/2/w/1200/format/webp)
![img](https://upload-images.jianshu.io/upload_images/3755117-ac654c5400c4a97e.png?imageMogr2/auto-orient/strip imageView2/2/w/1200/format/webp)

最短路径

最短路径问题

求单原点最短路径的 Dijikstra 算法

See the source image

# FindPath
Path[Start] = (Start,0)
for i in Graph[Start]:
    CanReach.append((Graph[Start][i],i,Start))
while(len(Arrive) < NodeNum):
    CanReach = sorted(CanReach)
    i = 0
    while(CanReach[i][1] in Arrive):
        i += 1
    at = CanReach[i][1]
    length = CanReach[i][0]
    Arrive.append(at)
    Path[at] = ((CanReach[i][2], CanReach[i][0]))
    for j in Graph[at]:
        if(j not in Arrive):
            CanReach.append((length + Graph[at][j],j,at))

求任意顶点间的最短路径的 Floyd 算法

基于图的邻接矩阵表示

  1. 若不允许经过任何中间点,则最短路径就是邻接矩阵
  2. 允许经过第一个顶点,与邻接矩阵比较,算出最小值
  3. 允许经过第一个和第二个顶点,…
def Floyd(graph):
    N = len(graph)
    for i in range(N): # pass point i
        for m in range(N):
            for n in range(N): #<m, n>
                old = graph[m][n]
                new = graph[m][i] + graph[i][n]
                if(new < old):
                    graph[m][n] = new
    return graph

inf = 1000
Graph = [[0,2,6,4],
         [inf,0,3,inf],
         [7,inf,0,1],
         [5,inf,12,0]]
print(Floyd(Graph))

AOV / AOE 网及其算法

AOV 网,又称顶点活动网(activity on vertex network),表示各项活动之间的先后顺序关系

AOV 网、拓扑排序和拓扑序列

拓扑排序和拓扑序列

拓扑排序 S:如果 N 中存在顶点 vivj 的路径,那么 Svi 就排在 vj 之前

拓扑序列不唯一

拓扑序列不包含回路

拓扑排序算法

算法思路

  1. N 中选出一个入度为 0 的顶点作为序列的下一顶点
  2. N 网中删除所选顶点及其所有的出边
  3. 反复执行上述步骤,直至已经选出了所有图中的顶点

代码

# get input
temp = input().split()
NodeNum = int(temp[0])
PathNum = int(temp[1])
Start = int(temp[2])
End = int(temp[3])
# Create Graph
Graph = dict()
ReGraph = dict()
From = dict()
EE = dict()
LE = dict()
Topology = [Start]
for i in range(NodeNum):
    Graph[i+1] = dict()
    From[i+1] = []
    ReGraph[i+1] = dict()
# Find Topology Order
now = Start
while not(now == End):
    for i in From:
        if now in From[i]:
            From[i].remove(now)
        if ((len(From[i]) == 0) and not (i in Topology)):
            Next = i
    Topology.append(Next)
    now = Next

AOE 网和关键路径

AOE 网 (Activity On Edge Network) 是另一类带权有向图

抽象来看,AOE 网是一种无环带权有向图,其中:

  • 顶点表示事件,有向边表示活动,边上的权值表示活动的持续时间
  • 图中一个顶点表示的事件,也就是它的入边所表示的活动都已完成,它的出边活动可以开始的那个状态。
  • AOE 网中描述的活动可以并行地执行。

关键路径:完成整个工程所需的最短时间,就是从开始顶点到完成顶点的最长路径的长度。

关键路径算法

定义变量

  • 事件 vj 最早可能发生时间 ee[j]
    • ee[0] = 0(初始时间总是在 0 时刻发生)
    • ee[j] = max{ee[i] + w[i, j]}
  • 事件 vj 最迟允许发生时间 le[j]
    • 根据已知 ee[j] 反向推算
    • le[n - 1] = ee[n - 1] (最后一个事件绝不能再延迟)
    • le[i] = min{le[j] + w[i, j]}

定义概念

  • 关键活动
    • ee[j] == le[j]
  • 时间余量
    • t[j] = le[j] - ee[j]

关键路径:算法

  1. 生成 AOE 网的一个拓扑序列
  2. 按照拓扑正序,生成 ee 表的值
  3. 按照拓扑逆序,生成 le 表的值
  4. el 一起计算,得到关键路径
import copy
def DFS(now, path):
    global Result, Critical, Graph, End
    if(now == End):
        Result.append(copy.copy(path))
        return
    for i in Graph[now]:
        if((i in Critical) and (Graph[path[-1]][i] == EE[i] - EE[path[-1]])):
            path.append(i)
            DFS(i, path)
            path.remove(i)
    return
# get input
temp = input().split()
NodeNum = int(temp[0])
PathNum = int(temp[1])
Start = int(temp[2])
End = int(temp[3])
# Create Graph
Graph = dict()
ReGraph = dict()
From = dict()
EE = dict()
LE = dict()
Topology = [Start]
for i in range(NodeNum):
    Graph[i+1] = dict()
    From[i+1] = []
    ReGraph[i+1] = dict()
for i in range(PathNum):
    temp = input().split()
    Graph[int(temp[0])][int(temp[1])] = int(temp[2])
    From[int(temp[1])].append(int(temp[0]))
    ReGraph[int(temp[1])][int(temp[0])] = int(temp[2])
print(Graph)
print(From)
# Find Topology Order
now = Start
while not(now == End):
    for i in From:
        if now in From[i]:
            From[i].remove(now)
        if ((len(From[i]) == 0) and not (i in Topology)):
            Next = i
    Topology.append(Next)
    now = Next
print(Topology)
# Find Critical Path
for i in Topology:
    if(i == Start):
        EE[Start] = 0
    else:
        can = []
        for j in ReGraph[i]:
            can.append(EE[j] + ReGraph[i][j])
        EE[i] = max(can)
print(EE)
Topology.reverse()
for i in Topology:
    if(i == End):
        LE[End] = EE[End]
    else:
        can = []
        for j in Graph[i]:
            can.append(LE[j] - Graph[i][j])
        LE[i] = min(can)
print(LE)
# Critical Path
Critical = []
for i in Graph:
    if(EE[i] == LE[i]):
        Critical.append(i)
Result = []
p = [Start]
DFS(Start, p)
Result = sorted(Result)
print(Result)

查找

顺序查找

从表的一端开始逐个将记录的关键字和给定K值进行比较,若某个记录的关键字和给定K值相等,查找成功;否则,若扫描完整个表,仍然没有找到相应的记录,则查找失败。

折半查找

折半查找又称为二分查找,是一种效率较高的查找方法。 前提条件:查找表中的所有记录是按关键字有序(升序或降序) 。 查找过程中,先确定待查找记录在表中的范围,然后逐步缩小范围(每次将待查记录所在区间缩小一半),直到找到或找不到记录为止。

索引查找

分块查找(Blocking Search)又称索引顺序查找,是前面两种查找方法的综合。

索引树

B_树

一棵 m 阶 B 树或者为空,或者具有下面特征:

  • 树中分支结点至多有 m-1 个排序存放的关键码。根结点至少有一个关键码,其他结点至少有 (m1)/2 个关键码
  • 如果一个分支节点有 j 个关键码,它就有 j + 1 棵子树,这一结点中保存的是一个序列 <p0,k0,p1,k1,,pj1,kj1,pj>, 其中 kj 为关键码,pj 为子结点引用,而且 ki 大于 pi 所引子树里所有的关键码,小于 pi+1 所引子树里所有的关键码

B_ 树图文详解

B+树

B+ 树图文详解

平衡二叉树 AVL

定义和性质

平衡二叉排序树是一类特殊的二叉排序树,它或为孔数,或者其左右子树都是平衡二叉排序树,而且其左右子树的高度之差的绝对值不超过 1

平衡因子 BF(Balance Factor):该结点的左子树高度减去右子树高度之差,可能的取指只有 1, -1, 0

AVL 树类

如果能维持平衡二叉树的结构,检索操作就能在 O(logn) 时间内完成

基本定义

为了实现 AVL 树,每个结点里需要增加一个平衡因子记录

插入操作

插入后的失衡与调整

  • 不失衡的情况
    • 若在检索树的过程中,所有途径的结点 BF 均为 0,那么实际上插入结点也不会导致失衡
  • 失衡的情况
    • 若失衡,则一定存在一棵包含实际插入点的最小非平衡子树,即包含新结点插入位置的、其根节点的 BF 非零的最小子树。如果插入新结点后这颗子树仍保持平衡,而且其高度不变,那么整棵二叉排序树也将保持平衡(由于该子树的高度不变,在它外面的树的结点的 BF 值都不变)。进一步说,如果插入新结点后的结构调整和 BF 值修改都能在子树内部的一条路径上完成,插入的复杂度将不超过 O(logn)
    • 类型
      • LL 型调整【a 的左子树较高,新结点插入在 a 的左子树的左子树】
      • LR 型调整【a 的左子树较高,新结点插入在 a 的左子树的右子树】
      • RR 型调整【a 的右子树较高,新结点插入在 a 的左子树的右子树】
      • RL 型调整【a 的右子树较高,新结点插入在 a 的左子树的左子树】
    • 在插入新结点并完成调整之后,这棵子树与插入之前这个位置上的子树高度相同,其结构变化对子树之外的部分无影响

LL(RR) 失衡与调整

  • LL

    image-20210109104205737

    在这里插入图片描述

  • RR

    image-20210109104640685

在这里插入图片描述

LR(RL) 失衡和调整

  • LR

    image-20210109111448478

  • RL

    image-20210111155857041

插入操作的实现

  1. 查找新结点的插入位置,并在查找过程中记录遇到的最小不平衡子树的根
    • 用一个变量 a 记录距插入位置最近的平衡因子非零的结点,由于可能需要修改这棵子树,在此过程中用另一变量 pa 记录 a 的父结点
    • 如果不存在这种结点,需要考虑的 a 就是树根
    • 如果在新结点插入后出现失衡,a 就是平衡位置
    • 实际插入新结点
  2. 修改从 a 的子结点到新结点的路径上各结点的平衡因子
    • 由于 a 的定义,这段结点原来都有 BF = 0
    • 插入后用一个扫描变量 p 从 a 的子结点开始遍历,如果新结点插入在 p 的左子树,就把 p 的平衡因子改为 1,否则改为 -1
  3. 检查以 a 为根的子树是否失衡,失衡时做出调整
    • 如果 a.bf == 0,插入后不会失衡,简单修改平衡因子并结束
    • 如果 a.bf == 1,而且新结点插入其左子树,就出现了失衡
      • 新结点在 a 的左子节点的左子树时做 LL 调整
      • 新结点在 a 的右子节点的左子树时做 LR 调整
    • 如果 a.bf == -1,而且新结点插入其右子树,就出现了失衡
      • 新结点在 a 的左子节点的左子树时做 RL 调整
      • 新结点在 a 的右子节点的左子树时做 RR 调整
  4. 连接好调整后的子树,它可能作为整棵树的根,或作为 a 原来的父节点的相应方向的子结点(左子结点或右子结点)

代码实现

class AVLNode(object):
    def __init__(self, value, left, right, bf):
        self.value = value
        self.left = left
        self.right = right
        self.bf = bf

def LL(a, b): # LL 型调整
    a.left = b.right
    b.right = a
    a.bf = b.bf = 0
    return b

def RR(a, b): # RR 型调整
    a.right = b.left
    b.left = a
    a.bf = b.bf = 0
    return b

def LR(a, b): # LR 型调整
    c = b.right
    a.left, b.right = c.right, c.left
    c.left, c.right = b, a
    if c.bf == 0: # c 本身就是插入结点
        a.bf= b.bf= 0
    elif c.bf == 1: # 新结点在 c 的左子树
        a.bf = -1
        b.bf = 0
    else: # 新结点在 c 的右子树
        a.bf = 0
        b.bf = 1
    c.bf = 0
    return c

def RL(a, b): # RL 型调整
    c = b.left
    a.right, b.left = c.left, c.right
    c.left, c.right = a, b
    if c.bf == 0: # c 本身就是新结点
        a.bf = 0
        b.bf = 0
    elif c.bf == 1: # 新结点在 c 的左子树
        a.bf = 0
        b.bf = -1
    else: # 新结点在 c 的右子树
        a.bf = 1
        b.bf = 0
    c.bf = 0
    return c
        
def insert(root, value):
    a = p = root
    if a is None: # 若是一棵空树
        root = AVLNode(value, None, None, 0)
        return
    pa = q = None # 维持 pa, q 为 a, p 的父节点
    while p is not None:
        if p.bf != 0:
            pa, a = q, p # 已知最小非平衡子树
        q = p
        if value < p.value:
            p = p.left
        else:
            p = p.right
    # q 是插入点的父节点, pa, a记录最小非平衡子树
    node = AVLNode(value, None, None, 0)
    if value < q.value:
        q.left = node # 作为左子结点
    else:
        q.right = node # 作为右子结点
    # 新结点已插入,a 是最小不平衡子树
    if value < a.value: # 新结点在 a 的左子树
        p = b = a.left
        d = 1
    else:
        p = b = a.right
        d = -1
    # 修改 b 到新结点路径上各结点的 bf 值, b 为 a 的子结点
    while p != node: # node 一定存在,不用判断 b 空
        if value < p.value: # p 的左子树增高
            p.bf =  1
            p = p.left
        else:
            p.bf = -1
            p = p.right
    if a.bf == 0: # a 的原 bf 为 0,不会失衡
        a.bf = d
        return
    if a.bf == -d: # 新结点在较低子树里
        a.bf = 0
        return
    # 新结点在较高子树,失衡,必须调整
    if d == 1: # 新结点在 a 的左子树
        if b.bf == 1:
            b = LL(a, b) # LL
        else:
            b = LR(a, b) # RL
    else: # 新结点在 a 的右子树
        if b.bf == -1:
            b = RR(a, b) # RR 调整
        else:
            b = RL(a, b) # RL 调整
    if pa is None: # 原 a 为树根,修改 root
        root = b
    else:
        if pa.left == a:
            pa.left = b
        else:
            pa.right = b
# 以下为加入一些遍历与输入操作后的代码
# -*- coding: utf-8 -*-
"""
Created on Sat Jan  9 19:45:52 2021

@author: Ericaaaaaaaa
"""

class AVLNode(object):
    def __init__(self, value, left, right, bf):
        self.value = value
        self.left = left
        self.right = right
        self.bf = bf
    def __str__(self):
        return "[AVLNode value: {0} bf: {1}]".format(self.value, self.bf)
    
def insert(root, value):
    a = p = root
    if a is None:
        root = AVLNode(value, None, None, 0)
        return
    pa = q = None # 维持 pa, q 为 a, p 的父节点
    while p is not None:
        if p.bf != 0:
            pa, a = q, p # 已知最小非平衡子树
        q = p
        if value < p.value:
            p = p.left
        else:
            p = p.right
    # q 是插入点的父节点, pa, a记录最小非平衡子树
    node = AVLNode(value, None, None, 0)
    if value < q.value:
        q.left = node # 作为左子结点
    else:
        q.right = node # 作为右子结点
    # 新结点已插入,a 是最小不平衡子树
    if value < a.value: # 新结点在 a 的左子树
        p = b = a.left
        d = 1
    else:
        p = b = a.right
        d = -1
    # 修改 b 到新结点路径上各结点的 bf 值, b 为 a 的子结点
    while p != node: # node 一定存在,不用判断 b 空
        if value < p.value: # p 的左子树增高
            p.bf =  1
            p = p.left
        else:
            p.bf = -1
            p = p.right
    if a.bf == 0: # a 的原 bf 为 0,不会失衡
        a.bf = d
        return
    if a.bf == -d: # 新结点在较低子树里
        a.bf = 0
        return
    # 新结点在较高子树,失衡,必须调整
    if d == 1: # 新结点在 a 的左子树
        if b.bf == 1:
            b = LL(a, b) # LL
        else:
            b = LR(a, b) # RL
    else: # 新结点在 a 的右子树
        if b.bf == -1:
            b = RR(a, b) # RR 调整
        else:
            b = RL(a, b) # RL 调整
    if pa is None: # 原 a 为树根,修改 root
        root = b
    else:
        if pa.left == a:
            pa.left = b
        else:
            pa.right = b
    
def LL(a, b):
    a.left = b.right
    b.right = a
    a.bf = b.bf = 0
    return b

def RR(a, b):
    a.right = b.left
    b.left = a
    a.bf = b.bf = 0
    return b

def LR(a, b):
    c = b.right
    a.left, b.right = c.right, c.left
    c.left, c.right = b, a
    if c.bf == 0: # c 本身就是插入结点
        a.bf= b.bf= 0
    elif c.bf == 1: # 新结点在 c 的左子树
        a.bf = -1
        b.bf = 0
    else: # 新结点在 c 的右子树
        a.bf = 0
        b.bf = 1
    c.bf = 0
    return c

def RL(a, b):
    c = b.left
    a.right, b.left = c.left, c.right
    c.left, c.right = a, b
    if c.bf == 0: # c 本身就是新结点
        a.bf = 0
        b.bf = 0
    elif c.bf == 1: # 新结点在 c 的左子树
        a.bf = 0
        b.bf = -1
    else: # 新结点在 c 的右子树
        a.bf = 1
        b.bf = 0
    c.bf = 0
    return c

def print_tree(root):
    queue = []
    now = root
    while not((len(queue) == 0) and (now == None or now.value == None or now.value == "None")):
        print(now)
        left = now.left
        right = now.right
        if not(left == None or left.value == None or left.value == "None"):
            queue.append(left)
        if not(right == None or right.value == None or right.value == "None"):
            queue.append(right)
        if(len(queue) == 0):
            now = None
        else:
            now = queue.pop(0)
        
initial = int(input())
root = AVLNode(initial, None, None, 0)
while True:
    l = input()
    if l == "finish":
        break
    else:
        insert(root, int(l))
        print_tree(root)

内部排序问题和性质

问题定义

排序算法

基于比较的排序

基本操作、性质和评价

在讨论各个算法时,总是以被排序序列的长度(即序列中元素的个数)作为问题的规模参数 n

  • 任何算法的时间复杂度都不可能优于 O(nlogn)
  • 算法的性质
    • 稳定性
      • 对于待排序序列里的任一对排序码相同的记录 RiRj,在排序后的序列里 RiRj 的前后顺序不变
      • 稳定性是一个具体算法的性质,而不是排序方法的性质
    • 适应性
      • 如果一个排序算法对接近有序的序列工作的更快,就称这种算法具有适应性

简单排序算法

插入排序

算法的思路

  1. 从一个没有元素的列表开始
  2. 选择一个未排序的元素
  3. 将所选元素与列表中的元素一一比较,并插入到正确的位置
  4. 重复 2、3 直至所有元素都被插入到列表中为止。

代码

def insert_sort(lst):
    for i in range(1, len(lst)): # 开始时片段 [lst[0]] 已排序
        x = lst[i] # 选择元素
        j = i
        while j > 0 and lst[j-1] > x: # 逐一向前比较
            lst[j] = lst[j-1] # 反序逐个后移元素,决定插入位置
            j -= 1
        lst[j] = x
    return lst # 有没有都行,因为 python 其实已经改变了原有的 lst 了

复杂度分析

  • 平均时间复杂度O(n2)
  • 最坏时间复杂度O(n2)
  • 空间复杂度O(1)

算法特性分析

  • 有稳定性

    lst[j-1] > x

  • 有适应性

改进

采用二分法检索插入位置

选择排序

算法思路

  1. 顺序扫描未排序序列中的元素,记住遇到的最小的元素
  2. 将最小元素于未排序的第一位交换
  3. 重复 1、2,直至序列排序完毕

代码

def select_sort(lst):
    """选择排序"""
    for i in range(len(lst) - 1): # 只需循环 len(lst) - 1 次
        k = i
        for j in range(i, len(lst)): # k 是已知最小元素的位置
            if lst[j] < lst[k]:
                k = j
        if i != k: # lst[k] 是已知确定最小的元素,检查是否需要交换
            lst[i], lst[k] = lst[k], lst[i] # 交换
    return lst # 有没有都行,因为 python 其实已经改变了原有的 lst 了

复杂度分析

  • 平均时间复杂度O(n2)
  • 最坏时间复杂度O(n2)
  • 空间复杂度O(1)

算法特性分析

  • 没有适应性

    任何情况下的时间复杂度都是 O(n2)

堆排序

补充知识

优先队列

优先队列是一种缓存结构,保证在任何时候访问或弹出的,总是当时这个结构里保存的所有元素里优先级最高的(在存数数据时会同时存入优先级)

树形结构和堆
堆及其性质
  • 采用树形结构实现优先队列的一种有效技术称为堆。
  • 从结构上看,堆就是结点里存储数据的完全二叉树
  • 堆序:任意一个结点里存储的数据的优先级先于(或等于)其子节点里的数据
    • 堆中优先级最高的元素必在堆顶
    • 大顶堆 & 小顶堆
优先队列的堆实现
插入元素和向上筛选
弹出元素和向下筛选

算法思路

代码

def heap_sort(elems): # 堆排序
    """
    堆排序:
    采用小顶堆,因此输出顺序为从大到小
    若希望得到从小到大的输入,只需要将 ① 与 ② 处改为 ">" 即可
    """
    def siftdown(elems, e, begin, end): # elems 按层序方式存储的堆,e 为要插入的元素(向下筛选),begin, end 为已有堆的 begin, end 下标
        i, j = begin, begin*2+1 # j 为 i 的左子结点
        while j < end: # invariant: j == 2 *i + 1
            if j+1 < end and elems[j+1] < elems[j]: # ① # 使得 j 为 i 子结点中最小的结点的下标
                j += 1 # elems[j] 小于等于其兄弟结点的数据
            if e < elems[j]: # e 在三者中最小 ②
                break
            elems[i] = elems[j] # elems[j] 最小,上移
            i, j = j, 2*j+1 # i 下移, j 为 i 的左子结点
        elems[i] = e # 将 e 放入合适的位置(i 处的元素已经被移走)
        
    end = len(elems)
    # 循环建堆
    for i in range(end//2, -1, -1): # 从最下层开始,逐步向上使得序列满足小顶堆条件
        siftdown(elems, elems[i], i, end)
    # 循环逐个取出最小元素,将其积累在表的最后,放一个退一步
    for i in range((end-1), 0, -1):
        e = elems[i]
        elems[i] = elems[0]
        siftdown(elems, e, 0, i)

复杂度分析

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(1)

算法特性分析

交换排序(冒泡排序)

算法思路

通过交换元素消除逆序

代码

def bubble_sort(lst):
    """冒泡排序"""
    for i in range(len(lst)):
        found = False
        for j in range(1, len(lst) - i):
            if lst[j-1] > lst[j]: # 找到逆序
                lst[j-1], lst[j] = lst[j], lst[j-1]
                found =True
        if not found: # 如果序列中已经没有逆序了
            break
    return lst # 有没有都行,因为 python 其实已经改变了原有的 lst 了

复杂度分析

  • 平均时间复杂度O(n2)
  • 最坏时间复杂度O(n2)
  • 空间复杂度O(1)

算法特性分析

  • 有稳定性

  • 有适应性

    if not found:

快速排序

算法思路

  1. 若序列长度为 0 或 1,证明已经完成排序,返回,若不然,执行 2
  2. 取待排序序列中的任意一个元素(通常是第一个)作为标准
  3. 将其他元素与之比较,并分成【比标准小】、【比标准大】两部分
  4. 将分好的两部分视为新的未排序序列,递归执行 2 操作

代码

def qsort_rec(lst, l, r):
    if l >= r:
        return # 分段无记录或只有一个记录
    i, j = l, r
    pivot = lst[i] # lst[i] 是初始空位
    while i < j: # 找 pivot 的最终位置
        while i < j and lst[j] > pivot:
            j -= 1 # 用 j 向左扫描找小于 pivot 的记录
        if i < j:
            lst[i] = lst[j]
            i += 1 # 小记录移到左边
        while i < j and lst[i] <= pivot:
            i += 1 # 用 i 向右扫描找大于 pivot 的记录
        if i < j:
            lst[j] = lst[i]
            j -= 1 # 大记录移到右边
    lst[i] = pivot # 将 pivot 存入其最终位置
    qsort_rec(lst, l, i-1) # 递归处理左半区间
    qsort_rec(lst, i+1, r) # 递归处理右半区间

lst = [1,6,4,3,2,7,8,9,5]
qsort_rec(lst, 0, len(lst) - 1)
print(lst)

复杂度分析

  • 平均时间复杂度: O(nlogn)
  • 最坏时间复杂度:O(n)
  • 空间复杂度:O(logn)

算法特性分析

  • 不稳定
  • 不具有适应性

归并排序

算法思路

  1. 开始时,将每个记录看成单独的有序序列,则 n 个待排序的记录就是 n 个长度为 1 的有序子序列
  2. 对所有有序子序列进行两两归并,得到 n/2 个长度为 2 或 1 的有序子序列——一趟归并
  3. 重复 2,直到得到长度为 n 的有序序列为止。

代码

def merge(lfrom, lto, low, mid, high):
    """
    归并排序最下层函数
    实现表中相邻的一对有序序列的归并工作,将归并的结果存入另一个顺序表里的相同位置
    需要归并的两有序段分别为:lfrom[low:mid] 和 lfrom[mid:high]
    归并结果应存入 lto[low:high]
    """
    i, j, k = low, mid, low # i, j 遍历两个有序子序列,k 写入结果序列
    while i < mid and j < high: # 反复赋值两分段首最小的
        if lfrom[i] <= lfrom[j]:
            lto[k] = lfrom[i]
            i += 1
        else:
            lto[k] = lfrom[j]
            j += 1
        k += 1
    while i < mid: # 复制第一段剩余记录
        lto[k] = lfrom[i]
        i += 1
        k += 1
    while j < high: # 复制第二段剩余记录
        lto[k] = lfrom[j]
        j += 1
        k += 1
        
def merge_pass(lfrom, lto, llen, slen):
    """
    归并排序中间层函数
    实现对整个表里顺序各对有序序列的归并,完成一遍归并,
    各对序列的归并结果顺序存入另一顺序表里的同位置分段
    slen: 需要归并的每小段长度
    llen: 序列总长度
    """
    i = 0
    while i + 2 * slen < llen: # 归并长 slen 的两段
        merge(lfrom, lto, i, i + slen, i + 2 * slen)
        i += 2 * slen
    if i + slen < llen: # 剩下两端,后段长度小于 slen
        merge(lfrom, lto, i, i + slen, llen)
    else: # 只剩下一段,复制给表 lto
        for j in range(i, llen):
            lto[j] = lfrom[j]
            
def merge_sort(lst):
    """
    归并排序主函数(最顶层函数)
    在两个顺序表中往复执行中间层操作,直至排序全部完成
    """
    slen, llen = 1, len(lst)
    templst = [None] * llen
    while slen < llen: # 未形成长度为总长度的顺序序列
        merge_pass(lst, templst, llen, slen)
        slen *= 2
        # 排序完成时,结果可能存放在 templst 中,无论如何,再执行一次下一步,将结果存回 lst 中
        merge_pass(templst, lst, llen, slen) # 结果存回原位
        slen *= 2

复杂度分析

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n)

算法特性分析

  • 有稳定性
  • 无适应性

其他排序方法

分配排序和基数排序

算法思路

如果关键码只有很少几个不同的值,

  1. 为每个关键码设置一个桶
  2. 遍历序列,根据关键码把记录放在不同的桶中
  3. 顺序手机各个桶的记录,得到排序的序列

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

多轮分配和排序

算法思路

  • 高位优先(Most Significant Digit first, MSD)
  • 低位优先(Least Significant Digit first, LSD)

希尔排序 Shell Sort

算法思路

  1. 先取一个正整数d1(d1<n)作为第一个增量,将全部n个记录分成d1组,把所有相隔d1的记录放在一组中,即对于每个k(k=1, 2, … d1),R[k], R[d1+k], R[2d1+k] , …分在同一组中,在各组内进行直接插入排序。这样一次分组和排序过程称为一趟希尔排序;
  2. 取新的增量d2<d1,重复 1 的分组和排序操作;直至所取的增量di=1为止,即所有记录放进一个组中排序为止。

image-20210106192442516

image-20210106193116456

外部排序

文件

文件的组织方式

顺序文件

索引文件

索引结构(称为索引文件)由索引表和数据表两部分

  • 数据表:存储实际的数据记录
  • 索引表:存储记录的关键字和记录(存储)地址之间的对照表,每个元素称为一个索引项
稠密索引
非稠密索引

ISAM

ISAM(Indexed Sequential Access Method,顺序索引存取方法),是专为磁盘存取设计的一种文件组织方式,采用静态索引结构,是一种三级索引结构的顺序文件。

image-20210106203126494

image-20210106203143277

VSAM

VSAM(Virtual Storage Access Method,虚拟存取方法),也是一种索引顺序文件组织方式,利用OS的虚拟存储器功能,采用的是基于B+树的动态索引结构。

image-20210106203231777image-20210106203249607

散列文件

散列文件(直接存取文件) :利用散列存储方式组织的文件。类似散列表,即根据文件中记录关键字的特点,设计一个散列函数和冲突处理方法,将记录散列到存储介质上。

多关键字文件

多重表文件

多重表文件(Multilist Files)的特点是:记录按主关键字的顺序构成一个串联文件(物理上的) ,并建立主关键字索引(称为主索引);对每个次关键字都建立次关键字索引(称为次索引),所有具有同一次关键字值的记录构成一个链表(逻辑上的)。

倒排文件

倒排文件又称逆转表文件。与多重表文件类似,可以处理多关键字查询。

外部排序

外部排序最基本的方法是归并。这种方法是由两个相对独立的阶段组成: ① 按内存(缓冲区)的大小,将n个记录的数据文件分成若干个长度为l的段或子文件,依次读入内存并选择有效的内部排序方法进行排序;然后将排好序的有序子文件重新写入到外存。子文件称为归并段或顺串。 ② 采用归并的办法对归并段进行逐趟归并,使归并段的长度逐渐增大,直到最后合并成只有一个归并段的文件—排好序的文件。

内存管理

兄弟伙伴算法

伙伴系统是一种非顺序内存管理方法,不是以顺序片段来分配内存,是把内存分为两个部分,只要有可能,这两部分就可以合并在一起; 且这两部分从来不是自由的,程序可以使用伙伴系统中的一部分或者两部分都不使用。与边界标识法类似,所不同是:无论占用块或空闲块,其大小均为2的k次幂。

当程序释放所占用的块时,系统将该新的空闲块插入到可利用空闲表中,需要考虑合并成大块问题。在伙伴系统中,只有“互为伙伴”的两个子块均空闲时才合并;即使有两个相邻且大小相同的空闲块,如果不是“互为伙伴” (从同一个大块中分裂出来的)也不合并。

设要回收的空闲块的首地址是p,其大小为2k的,算法思想是: ⑴ 判断其 “互为伙伴”的两个空闲块是否为空: 若不为空,仅将要回收的空闲块直接插入到相应的子表中;否则转⑵; ⑵ 按以下步骤进行空闲块的合并: ◆ 在相应子表中找到其伙伴并删除之; ◆ 合并两个空闲块; ⑶ 重复⑵,直到合并后的空闲块的伙伴不是空闲块为止。6