大话数据结构

712 阅读33分钟

阶段一:认识数据结构

十大数据结构:数组、链表、栈、队列、散列表、跳表、图、树、堆、字典树

数组

数组是最简单、也是使用最广泛的数据结构。栈、队列等其他数据结构均由数组演变而来。

基本形式

索引与元素对应,一般索引都从0开始

两种类型:一维数组、多维数组 / 静态数组、动态数组

基本操作

  • Insert——在指定索引位置插入一个元素

  • Get——返回指定索引位置的元素

  • Delete——删除指定索引位置的元素

  • Size——得到数组所有元素的数量

面试常见问题

  • 寻找数组中第二小的元素

  • 找到数组中第一个不重复出现的整数

  • 合并两个有序数组

  • 重新排列数组中的正值和负值

经典的撤销操作就是基于栈来实现的

基本形式

后进先出LIFO

可以把栈想象成一列垂直堆放的书。为了拿到中间的书,你需要移除放置在这上面的所有书。

基本操作

  • Push——在顶部插入一个元素

  • Pop——返回并移除栈顶元素

  • isEmpty——如果栈为空,则返回true

  • Top——返回顶部元素,但并不移除它

面试常见问题

  • 使用栈计算后缀表达式

  • 对栈的元素进行排序

  • 判断表达式是否括号平衡

队列

一种顺序存储元素的线性数据结构

基本形式

先进先出FIFO

售票亭排队队伍,如果有新人加入,他需要到队尾去排队,排在前面的人会先拿到票,然后离开队伍。

基本操作

  • Enqueue()——在队列尾部插入元素

  • Dequeue()——移除队列头部的元素

  • isEmpty()——如果队列为空,则返回true

  • Top()——返回队列的第一个元素

面试常见问题

  • 使用队列表示栈

  • 对队列的前k个元素倒序

  • 使用队列生成从1到n的二进制数

链表

同样是线性结构,但其存储方式不连续,用于实现文件系统。

基本形式

链表就像一个节点链,其中每个节点包含着数据和指向后续节点的指针。

链表还包含一个头指针,它指向链表的第一个元素,但当列表为空时,它指向null或无具体内容。

两种类型:单向链表、双向链表、循环链表

基本操作

  • InsertAtEnd - 在链表的末尾插入指定元素

  • InsertAtHead - 在链接列表的开头/头部插入指定元素

  • Delete - 从链接列表中删除指定元素

  • DeleteAtHead - 删除链接列表的第一个元素

  • Search - 从链表中返回指定元素

  • isEmpty - 如果链表为空,则返回true

面试常见问题

  • 反转链表

  • 检测链表中的循环

  • 返回链表倒数第N个节点

  • 删除链表中的重复项

一组以网络形式相互连接的节点

基本形式

节点也称为顶点

一对节点(x,y)称为边(edge),表示顶点x连接到顶点y

边可以包含权重/成本,显示从顶点x到y所需的成本

两种类型:无向图、有向图

程序语言中的表现形式:邻接矩阵、邻接表

常见遍历算法:广度优先搜索、深度优先搜索

面试常见问题

  • 实现广度和深度优先搜索

  • 检查图是否为树

  • 计算图的边数

  • 找到两个顶点之间的最短路径

广泛应用于人工智能和复杂算法,它可以提供解决问题的有效存储机制

基本形式

Root - 根节点

Parent - 父节点

Child - 子节点

Leaf - 叶子节点

Sibling - 兄弟节点

树形结构是一种层级式的数据结构,类似于图,也由顶点(节点)和连接它们的边组成

与图的区别:树中不存在环路

主要类型

  • N元树

  • 平衡树

  • 二叉树

  • 二叉搜索树

  • AVL树

  • 红黑树

  • 2-3树

面试常见问题

  • 求二叉树的高度

  • 在二叉搜索树中查找第k个最大值

  • 查找与根节点距离k的节点

  • 在二叉树中查找给定节点的祖先节点

字典树

也称为“前缀树”,是一种特殊的树状数据结构,对于解决字符串相关问题非常有效

能够提供快速检索,主要用于搜索字典中的单词,在搜索引擎中自动提供建议,甚至被用于IP的路由

基本形式

这些单词以顶部到底部的方式存储,其中绿色节点“p”,“s”和“r”分别表示“top”,“thus”和“theirs”的底部。

面试常见问题

  • 计算字典树中的总单词数

  • 打印存储在字典树中的所有单词

  • 使用字典树对数组的元素进行排序

  • 使用字典树从字典中形成单词

  • 构建T9字典(字典树+ DFS )

哈希表

哈希表,是基于哈希法的数据结构 (哈希法:是一个用于唯一标识对象并将每个对象存储在一些预先计算的唯一索引(称为“键(key)”)中的过程)

对象以键值对的形式存储,这些键值对的集合被称为“字典”

基本形式

哈希表通常使用数组实现,数组的索引是通过哈希函数计算

散列数据结构的性能影响因素

  • 哈希函数

  • 哈希表的大小

  • 碰撞处理方法

面试常见问题

  • 在数组中查找对称键值对

  • 追踪遍历的完整路径

  • 查找数组是否是另一个数组的子集

  • 检查给定的数组是否不相交

跳表

针对链表的优化,跳过一部分节点以加快速度

基本形式

跳表就是以上层级的集合

基本操作

  • 查询:从最上层开始查,大于的就进入下一层,依次深入直到查询到
  • 插入:先查询,再在最底层插入,对于上面的层级是否要插入靠抛硬币(50%概率),0.5^(X-1)
  • 删除:先查询,再删除,同插入操作,从下往上

面试常见问题

  • 实现
  • 与红黑树的对比

堆是一种比较特殊的数据结构,可以被看做一棵树的数组对象

基本形式

定义:n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆, (ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2)

  • 堆中某个节点的值总是不大于或不小于其父节点的值

  • 堆总是一棵完全二叉树

类别:最大堆/最小堆(根节点的大小)、二叉堆、斐波那契堆

面试常见问题

  • 实现堆创建,插入及删除
  • 优先级队列
  • 100w个数中找到最大的前K个数
  • 堆排序

阶段二:深入数据结构

数据结构绪论

数据结构:是相互之间存在一种或多种关系的数据元素的集合

数据结构学科:是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科

为什么要学?

客户排队模块的开发,数据库表->数组->队列

基本概念和术语

  • 数据:能被输入到计算机中,能被程序处理(如整型、实型等数值类型,如可被编码为字符类型的声音、图像)
  • 数据元素:数据的集合,如人、动物
  • 数据项:数据不可分割的最小单位,如人的眼耳口鼻、姓名年龄
  • 数据对象:性质相同的数据元素的集合,如人类这个群体
  • 数据结构:是相互之间存在一种或多种关系的数据元素的集合

逻辑结构与物理结构

逻辑结构

  • 集合结构:数据元素同属一个集合,除此之外,它们间没有任何关系
  • 线性结构:数据元素是一对一的关系
  • 树形结构:数据元素是一对多的关系
  • 图形结构:数据元素是多对多的关系

物理结构

  • 顺序存储:数据元素放在地址连续的存储单元中,其数据间的逻辑关系和物理关系是一致的
  • 链式存储:数据元素放在任意的存储单元中,存储单元可以是连续的也可以是不连续的,需要指针来反映其逻辑关系

抽象数据类型

数据类型:一组性质相同的值的合集及定义在此集合上的一些操作的总称

(如原子类型:整型、实型、字符型,结构类型:整型数组)

抽象数据类型:Abstract Data Type,ADT,是指一个数据模型及定义在该模型上的一组操作

(抽象,顾名思义,仅表示逻辑特性,与计算机内部如何表示无关)

不仅包括数据类型,还包括操作,类似于“类”

算法

算法:是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作

特性

  • 输入输出:零个或多个输入,一个或多个输出
  • 有穷性:不会无限循环
  • 确定性:相同输入必须相同输出
  • 可行性:每一步都能执行有限次数完成/可以转换为程序执行

设计要求

  • 正确性:无语法错误,合法输入得到合理输出,非法输入得到满足规格说明的结果,刁难的数据测试都能满足
  • 可读性:便于阅读、理解、交流
  • 健壮性:对输入数据不合法做相关处理
  • 时间效率高和存储量低

时间复杂度

定义:

在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。

算法的时间复杂度记作:T(n)=O(f(n))

表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同。

  • 常数阶 O(1)
  • 对数阶 O(logn)
  • 线性阶 O(n)
  • nlogn阶 O(nlogn)
  • 平方阶 O(n^2)
  • 立方阶 O(n^3)
  • 指数阶 O(2^n)
  • O(n!)

以上时间复杂度由小到大

推导大O阶方法

  1. 用常数1取代运行时间中的所有加法常数
  2. 在修改后的运行次数函数中,只保留最高阶项
  3. 如果最高阶项存在且不是1,则去除与这个项相乘的常数

得到的结果就是大O阶

空间复杂度

定义

空间复杂度:通过计算算法所需的存储空间实现

S(n)= O(f(n)),n为问题的规模,f(n)为语句关于n所占存储空间的函数

计算

存储程序本身的指令、常数、变量和输入数据,还需要存储对数据操作的存储单元。

如果算法执行所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1)

线性表

定义

线性表:零个或多个元素的有限序列

  • 有且仅有零个或一个直接前驱与直接后继
  • 有限个,可以为零(空表)
  • 相同数据类型
  • 对于复杂的线性表,一个数据元素可以由若干个数据项构成,如班级花名册

抽象数据类型

对于不同的需求,线性表有不同的操作,常见操作如下

  • 初始化创建空表
  • 判断是否为空
  • 清空操作
  • 查询
  • 插入
  • 删除
  • 返回长度
  • 合并两线性表

线性表的顺序存储结构

其实就是C语言和C#语言中的一维数组

定义:用一段地址连续的存储单元依次存储线性表的数据元素

描述属性

  • 存储空间的起始位置:数组data,其存储位置剧算存储空间的存储位置
  • 最大存储容量:数组长度MaxSize
  • 当前长度:length

地址计算方式:Loc(ai) = Loc(a1) + (i-1)* c ———时间复杂度O(1)

基本操作

  • 获取元素的操作,思路:输入三个东东(线性表、索引值、保存返回数据的变量),先判断索引值若大于线性表长度或小于1,又或者线性表为空,则返回错误提示,否则就返回线性表对应索引上的值
  • 插入元素的操作,思路:输入三个东东(线性表,要插入的索引值、插入值),还是先判断索引值是否在正常范围以及线性表是否为空表,遍历索引值后的所有元素,使其后移一位(插入位置为表尾就跳过该步骤),再将索引值处的元素赋值为插入值,最后线性表长度加一
  • 删除元素的操作,思路:输入三个东东(线性表,要删除的索引值),还是先判断索引值是否在正常范围以及线性表是否为空表,取出要删除的元素,遍历索引值后的所有元素,使其前移一位(删除位置为表尾就跳过该步骤),最后线性表长度减一

插入与删除

  • 最理想情况是对最后一个元素进行操作,从而时间复杂度为O(1)
  • 最差情况是对第一个元素进行操作,从而时间复杂度为O(n)
  • 平均情况,为(n-1)/2,时间复杂度为O(n)

时间复杂度:存取操作时间复杂度为O(1),插入删除操作时间复杂度为O(n)

优缺点

线性表的链式存储结构

存储单元任意、存储信息多了一个后继元素的指针(存储地址)

官方概念

  • 数据域
  • 指针域:其中的东东叫做链或者指针
  • 存储映像/节点(Node):数据域+指针域
  • 链表:n个节点连接而成(单链表:每个节点只有一个指针域)
  • 头指针:链表第一个节点的存储位置
  • 最后一个节点的指针为空

单链表:每个节点只有一个指针域

单链表图解:

头指针与头结点

  • 头结点的数据域一般为空,但也可存储链表长度
  • 头结点的指针域指向第一个有数据域的节点
  • 头指针指向头节点
  • 头指针在头结点前面,且头指针永不为空,常作为链表名字

图解:

空表:

基本操作

  • 单链表的读取操作,思路:输入链表、索引位置、保存位置,先取第一个节点p,while循环,不断使p指向下一个节点,跳出条件为p为空或到达索引位置(因为不清楚链表长度,不好用for循环),取出索引位置节点即可,时间复杂度为O(n)
  • 单链表的插入操作,思路:输入链表、插入位置、插入节点,和读取操作一样,取得插入位置的节点p,将插入节点s.next指向p.next,再将p.next指向s,注意顺序不能反,否则会出现自己指向自己。emmmm,也有可能输入的是插入数据,那就要先生成一个空节点s,给s赋数据值,其他的和上面一样。时间复杂度O(n)
  • 单链表的删除操作,思路:输入链表、删除索引、删除元素保存位置,和读取操作一样,取得要删除索引的前一个节点p,让p.next指向p.next.next,这样就删除成功了。时间复杂度O(n)

效率PK

链式存储结构和顺序存储结构相比,插入和删除操作的时间复杂度都是O(n)

没办法,因为链式存储结构每次都要遍历到索引值处

但是,!!!,如果一次插入或删除多个元素,那链式存储结构只需第一个O(n),其他都是O(1)

结论:链式存储结构常用于需要反复插入和删除元素的情况中

单链表的整表创建

  • 头插法

    简单说,就是每次都将数据往表头插,但是最后得到的链表顺序与给的数据顺序是相反的

    思路:生成空节点L,空节点指针指向null,循环遍历,每次循环中新生成一个节点p,赋值数据域,将其指针域指向L.next,再将L.next指向p。

    图示

  • 尾插法

    简单说,就是每次都把数据往表尾插,但是记得,每次插完后记得保存最后的节点

    思路:生成空节点L,空节点指针指向null,生成空节点r指向L,循环遍历,每次循环中新生成一个节点p,赋值数据域,将r.next指向p,然后更新最末尾节点r为p。

    图示

单链表的整表删除

值得注意的是,不要天真地认为让L指向null就好了,这样内存中还是有数据的,只是你自己找不到了而已

思路:p指向第一个节点,q指向第二个节点,循环遍历,对p执行释放操作free(),再将p指向q,q指向q.next

单链表结构与顺序存储结构的比较

  • 存储分配方式:随机---连续
  • 时间性能:
    • 存取操作:O(n)---O(1)
    • 插入删除操作:O(1)---O(n),当然了,数据量很大时的近似
  • 空间性能:顺序存储结构容易浪费空间或者产生溢出问题,单链表则无

综上得出结论:

  • 使用单链表:若线性表需要频繁插入与删除,或者未知长度的线性表(举例:玩家装备列表)
  • 使用顺序存储结构:若线性表需要频繁查找,较少插入与删除(举例:玩家个人信息)

静态链表

简单理解,就是古时候没有指针这个东西,就只能用数组来模拟单链表了,这样就叫做静态链表

指针域存放的不是指针,是数字(下标)

图示:

  • 数组的第一个元素与最后一个元素不存放数据
  • 其余未存放数据的元素叫做备用链表
  • 第一个元素指向第一个未存放数据的位置
  • 最后一个元素指向第一个存放数据的位置,相当于头结点

静态链表的插入操作

目标:要想在第二(k)个节点,即数据为A的节点后插入数据为B的节点:

思路:先取数组的第一个元素的游标,即找到空闲节点的下标i,更新第一个元素的游标为下标i元素的游标值,为下标i元素赋值给定数据,取数据的最后一个元素的游标,即找到第一个有数据的节点的下标j,不断遍历找到第k-1个节点,即要插入位置的前一个节点,将下标i元素游标赋值为该节点的游标值,再将该节点的游标值更新为i,大功告成。

静态链表的删除操作

目标:删除第3个节点,即数据为C的节点

思路:根据最后一个元素的游标找到第一个有数据节点,遍历找到第2个节点(B),将其游标更新为第3个节点(C)的游标,再将第3个节点(C)的数据清空,游标更新为第一个元素的游标,第一个元素的游标更新为被删除元素的下标

静态链表的优缺点

  • 优点:数据插入和删除操作无需移动大量元素
  • 缺点:失去了顺序存储结构存取上高效的优点;其本质还是数组,还是存在无法确定表长的问题,事先要申请预估空间

实际上没啥用,只是解决某些编程语言无指针从而无法实现单链表的问题,重要的是其思想

循环链表

简单的说就是屁股指向头,形成一个“环”,抽象的说:单链表的终端节点的指针由空指针改为头结点

区别:与单链表的不同在于判断空表的情况,单链表的判定条件head.next==null,循环链表的判定条件head.next==head

操作:初始化、插入、删除、索引等操作其实和单链表差不多,就是对第一个元素进行操作时,要考虑到最后一个元素的指向(最后一个元素:遍历,当节点.next等于头结点时则为最后)

约瑟夫环:所有人围成一圈,开始报数,报到3的自杀,之后又从1开始报数,输出死亡顺序

改进(尾指针):采用尾指针的形式,这样获取头结点和尾节点都是O(1),否则获取头结点为O(1),尾节点为O(n)

判断链表中是否有环(不一定只指尾节点到头结点)

  • 方法一:

    节点p每次都向前走一步,记录已走步数,且所处节点

    节点q每次都重头开始走,直到走到p所处的节点,记录每次走的步数

    如果两者步数不同,则有环

  • 方法二:

    节点p走一步

    节点q走两步

    两者同时走,若无环则不相遇(或者终点相遇),有环则快的会追上慢的

双向链表

双向链表是在单链表的每个结点中再设置一个指向前驱节点的指针域

基本操作

  1. 查询、长度、获取位置,以上操作同单链表,只需用到单向指针
  2. 插入、删除,需要更改两个指针变量

注意插入和删除的顺序

  1. 往节点p后插入节点s
s.prior = p;
s.next = p.next;
p.next.prior = s;
p.next = s;
  1. 删除节点p
p.prior.next = p.next;
p.next.prior = p.prior;

说白了,双向链表就是用空间换时间

栈与队列

栈:仅在表尾进行插入和删除操作的线性表

队列:只允许在一端插入,另一端删除的线性表

栈的介绍

后进先出LIFO,应用:浏览器后退、软件撤销操作,特殊的线性表

  • 栈顶top:允许插入与删除的一端
  • 栈底bottom:另一端

进栈与出栈

进栈即为插入,出栈即为删除

1、2、3数字依次进栈,共有5种出栈次序

123
132
213
231
321

栈的抽象数据类型

同线性表,只不过插入和删除换个叫法,push和pop

栈的顺序存储结构

同样用数组来实现,还需定义一个top变量,是数组的最后一个元素的索引,而索引为0的一端作为bottom,这样变化会小一点

进栈出栈操作很简单,无需和线性表一样移动其他元素,时间复杂度为O(1)

缺点:存储空间大小固定

两栈共享空间

用一个数组来存储两个栈,栈1的栈底为数组[0],栈2的栈底为数组[n-1],两栈栈顶向中间延伸,栈满情况:top1 + 1 == top2

进栈与出栈,需要先判断栈是否会满,再对给定的栈进行操作

使用情况:两个栈的需求具有相反关系,前提是数据类型相同才能用

栈的链式存储结构

又称链栈。

栈顶放在链表的头部,变化会小一点。

同样的,操作如单链表,进栈和出栈针对top节点而已,时间复杂度O(1)

比较

顺序栈和链栈在进栈和出栈操作的时间复杂度都是O(1)

顺序栈:固定长度,可能会造成空间浪费,但存储方便(适用于元素变化在一定范围内的)

链栈:指针域会增加内存开销,但长度无限制(适用于元素变化不可预料的情况)

栈的应用

  1. 递归(斐波那契数列)

    递归:形象如两镜子相对,抽象如调用自身的函数

    必须有一个条件使递归不再进行,与迭代相对应

    • 递归使用的是选择结构,但大量递归调用会建立大量函数的副本,耗费时间与内存
    • 迭代使用的是循环结构

    斐波那契数列:1,1,2,3,5,8,13...(前面相邻两项之和构成后一项)

    关键代码return F(n-1) + F(n-2);

    递归与栈:编译器是使用栈来实现递归的,但对高级语言来说,无需关心,系统已代劳

  2. 四则运算表达式

    后缀逆波兰)表示法——RPN

    用来解决四则运算:先乘除、后加减、从左到右、先括号里后括号外

    中缀表达式“9+(3-1)×3+10÷2” ---->后缀表达式“9 3 1 - 3 * + 10 2 / +”

    (正因为运算符都在数字后面,才叫后缀表达式)

    计算方式:对后缀表达式,遇到数字就入栈,遇到符号就出栈两个元素再运算,将结果入栈,直到表达式结束(理论上,栈由空到非空,再到空)

    中缀表达式转后缀表达式:准备一个空栈,从左到右遍历中缀表达式,是数字就输出(不是入栈),是符号就判断与栈顶元素的优先级,若高于栈顶元素就入栈,否则先让栈顶元素出栈,再判断,还低就再出栈(出栈立即输出)。不输出括号,遇到右括号立即让原栈顶元素出栈直至匹配的左括号。

队列的介绍

先进先出FIFO,应用:操作系统、客服系统

  • 队尾:允许插入的一端
  • 队头:允许删除的一端

队列的抽象数据类型

同线性表,只不过插入(EnQueue)和删除(DeQueue)的位置固定

队列的顺序存储结构

用数组表示队列,索引为0表示队头,索引n-1表示队尾

入队操作时间复杂度O(1),出队操作时间复杂度O(n)

改进——循环队列

队列的顺序存储结构存在不足:出队列时间复杂度高

设想:队头不需要一定在下标为0的位置

循环队列:队列头尾相接的顺序存储结构

同样用数组来实现,定义front指向队头的索引,rear指向队尾的下一个索引(队列为空则指向0)

每次入队操作将元素放入rear所在索引处,rear++,出队操作将front索引处元素删除,front++(关键就是队头是移动的),注意是循环的,rear与front到达数组尾端后会从0开始

  • 循环队列满:(rear + 1) % QueueSize == front
  • 循环队列长度:(rear - front + QueueSize) % QueueSize

优缺点:入队出队时间复杂度都是O(1),但仍没用摆脱数组的局限,即长度固定,有溢出风险

队列的链式存储结构

和单链表类似,只不过头结点作为头指针front只能删除,终端节点作为尾指针rear只能插入

入队出队事件复杂度O(1)

比较

队列长度最大值确定,用循环队列,否则用链队列

串:由零个或多个字符组成的有限序列,也称字符串

概念

  • 空串“”
  • 空格串“ ”——只包含空格的串,可多个
  • 子串——串中任意个数的连续字符组成的子序列
  • 主串——包含字串的串

串的比较

ASCII编码:八位二进制,可表示256个字符

Uniciode编码:十六位二进制,其前256个字符和ASCII码一样

从头开始根据ASCII编码挨个比较

串的抽象数据类型

和线性表类似,但串中每个元素的数据类型都是字符型

在操作上侧重点也不同,线性表侧重对单个元素的增删改查,而串更注重多个元素的操作,如查找子串的位置,替换字串等

串的顺序存储结构

同样的,用定长数组来定义,某些语言中串的结尾用\0表示,不计入长度但占一个位置

缺点就是容易溢出,改进就是在程序执行过程中动态分配,利用“堆”

串的链式存储结构

为了节约空间,常常每个节点存放多个字符,对最后一个节点,未满的字符用#填充

总的来说,串的链式结构并不方便

朴素的模式匹配算法

思路:对主串进行大循环,每个字符开头做(字串)长度T的小循环,直到匹配成功或遍历完成。

主串长度n,字串长度m

  • 最好情况,一次过O(1)
  • 次好情况,每次头都不对,最好一次全匹配O(n+m)
  • 等概率原则(n+m)/2,平均O(n+m)
  • 最差情况,O((n-m+1)*m)

该算法实在低效,因为计算机中都是二进制存储,比较次数会很多

KMP模式匹配算法

过于复杂,暂时跳过

树的介绍

树:是n个结点的有限集,一对多关系

非空树

  • 有且仅有一个根结点Root
  • 其余节点可分为m个互不相交的有限集,称为子树

(如果结构中有两子树相交则该结构不是树)

基本概念

  • 度Degree,节点拥有的子树数
  • 叶结点(Leaf)/终端节点,度为0的结点
  • 分支结点/非终端结点,度不为0的结点
  • 内部结点 = 分支结点 - 根结点
  • 树的度 = Max{各结点度}

  • 层次(Level),根节点为第一层
  • 深度(Depth)/高度,树中结点的最大层次
  • 有序树,各子树从左到右是有次序的,不能互换的
  • 森林,m颗互不相交树的集合

结点间关系

孩子(Child),双亲(Parent),兄弟(Sibling),祖先,子孙

线性结构与树结构的对比

线性结构

  • 第一个数据元素无前驱
  • 最后一个数据元素无后继
  • 中间元素,一个前驱一个后继

树结构

  • 根节点无双亲,唯一的
  • 叶子节点无孩子,可多个
  • 中间节点,一个双亲多个孩子

树的存储结构

1. 双亲表示法

- 基本型-方便找双亲

data|parent

用数组实现,每个结点,除了根结点,数据域保存数据信息,指针域指向其双亲结点的数组下标。

获取双亲结点的时间复杂度:O(1)

获取孩子结点的时间复杂度:O(n),需要遍历整棵树

- 改进型-方便找孩子

data|parent|firstchild

新增一个长子域,用来保存最左边孩子的下标,方便孩子为0、1的结点

- 改进型-方便找兄弟

data|parent|rightsib

新增一个右兄弟域,用来保存右兄弟结点的下标

2. 孩子表示法

用链表实现,每个结点都有多个指针域,分别指向其子树的根节点

- 指针域个数固定

每个结点的指针域的个数固定,等于该树的度

缺点:如果每个结点的度相差太大,会浪费空间

- 指针域个数不固定

有多少孩子就有多少指针域,多个空间存储个数

缺点:结构不固定,还要维护结点的度的数值

- 孩子表示法

数组+链表 相结合

数组保存着所有的结点,链表则反映其孩子结点(存储其下标)

- 孩子表示法+双亲表示法

3. 孩子兄弟表示法

data|firstchild|rightsib

两个指针域,存放第一个孩子与兄弟结点

如果有必要还可加入一个指针域表示双亲结点

好处:变形为一颗二叉树

二叉树的介绍

二叉树:由n个结点构成的有限集合,该集合要么为空(即空二叉树),要么由一个根结点和两棵互不相交的,被称为根结点的左子树和右子树的二叉树构成。

特点

  • 每个结点度为0、1、2,最多为2
  • 区分左右,即使只有一个

形态

  • 空二叉树
  • 无子树
  • 只有左子树
  • 只有右子树
  • 都有

特殊二叉树

  1. 斜树

    左斜树(只有左子树)、右斜树(只有右子树)

    相当于线性表,线性表可以理解为特殊的树结构

  1. 满二叉树

    每个结点(除了叶子)都有左子树和右子树,且叶子结点都在同一层

  1. 完全二叉树

    编号为i的结点与同样深度的满二叉树编号为i的结点在二叉树中位置相同

二叉树的性质

  1. 二叉树的第i层上至多有2^(i-1)个结点

  2. 深度为k的二叉树至多有2^k -1个结点

  3. 假设ni表示度为i的结点数,则n0=n2+1

    推导:结点总数 = n0 + n1 + n2

    线段数 = 2×n2 + 1×n1

    结点数 = 线段数 + 1

  4. 具有n个结点的完全二叉树的深度为([log2n])+1,中括号表示不大于它的最大整数

二叉树的存储结构

顺序存储结构

用数组,将二叉树的每个结点依次放入

对于完全二叉树来说,没有空位,对于普通二叉树,缺少的地方用特殊字符补位

二叉链表

lchild|data|rchild

一个数据域,两个指针域

遍历二叉树

从根结点出发,按某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且有且仅被访问一次

1. 前序遍历

ABDGHCEIF,根-左-右

2. 中序遍历

GDHBAEICF,左-根-右

3. 后序遍历

GHDBIEFCA,左-右-根

4. 层序遍历

ABCDEFGHI

代码实现

递归

打印当前结点信息的语句 放于 递归左子树、递归右子树语句的 前、中、后

推导遍历结果

  • 已知前序和中序遍历,或者已知中序和后序遍历,可以确定一棵二叉树

  • 已知前序和后序遍历则无法得到

前序遍历的第一个元素为根结点,后序遍历的最后一个元素为根结点

中序遍历从根结点分开,左边为左子树,右边为右子树

依次推导即可

二叉树的建立

拓展二叉树:补充结点充盈整棵树,空节点用#表示

建立二叉树:根据前序、中序、后序遍历结果构建二叉树

同样用遍历,只不过是将打印语句换成构造结点的语句

线索二叉树

二叉树通常用二叉链表实现,即一个数据域,两个指针域

问题:对于某些结点(如叶子结点),指针域为空,浪费空间

解决:将空指针域用来存放某种遍历方式下的前驱结点和后继结点,为了区分指针域存放的是前驱后继结点还是孩子结点,再加上两个标志域来表示(占空间小)

概念

  • 线索:指向前驱/后继结点的指针
  • 线索链表:加上线索的二叉链表
  • 线索二叉树
  • 线索化:对二叉树以某种次序遍历使其成为线索二叉树的过程

好处:将二叉树转化为一个双向链表,对于需要经常遍历与查找结点的二叉树来说该结构很好使

树、森林、二叉树的转换

1. 树转换为二叉树

孩子兄弟表示法

2. 二叉树转换为树

关键:所有的右孩子要加线

3. 森林转换为二叉树

关键:先单独转,再依次作为右孩子节点

4. 二叉树转换为森林

关键:所有的右孩子去线

树的遍历

前序遍历:ABEFCDG

后序遍历:EFBCGDA

森林的遍历

前序遍历:ABCDEFGHJI

后序遍历:BCDAFEJHIG

赫夫曼树及其应用

应用:常用来压缩数据,提高搜索效率的

定义:赫夫曼树为带权路径长度WPL最小的二叉树,也称最优二叉树

二叉树a的WPL = 5×1 + 15×2 + 40×3 + 30×4 + 10×4 = 315

二叉树b的WPL = 5×3 + 15×3 + 40×2 + 30×2 + 10×2 = 220

构造赫夫曼树的步骤

也称赫夫曼算法

  1. 根据每个结点出现的概率从小到大排序
  2. 取前两个结点构成一棵树,概率相加后当作一个虚拟结点放回继续排序
  3. 仍取前两个结点构成一颗树,概率小的为左树,大的为右数
  4. 依次进行

抽象描述如下:

赫夫曼编码

根据字母出现的概率来定义其二进制字符(0/1)

编码步骤:

  1. 根据概率构造二叉树
  2. 将左分支的权值改为0,右分支的权值改为1
  3. 根据所经路径来编码

解码步骤:将一串数字带入赫夫曼树中,依次得到对应字母

图是由顶点的有穷非空集合和顶点之间的边的集合构成,通常表示为G(V,E),G表示一个图,V表示图G中顶点的集合,E表示图G中边的集合

线性表、树、图的对比

  • 关系:一对一、一对多、多对多
  • 数据元素名称:元素、结点、顶点
  • 可否为空:可,可,不可

基本概念

  • 无向边:用括号表示,如(A,B)或者(B,A)
  • 有向边:也成为弧,用尖括号表示,若顶点A指向B,则A为弧尾,B为弧头,如<A,B>
  • 无向图:所有的边都是无向边
  • 有向图:所有的边都是有向边
  • 简单图:不存在到自身的边,以及重复的边
  • 无向完全图:无向图中任意两顶点间都有边,总边数为n*(n-1)/2,推导:n个顶点,对剩下的n-1个顶点都有边,又因为是无向,所以除2
  • 有向完全图:有向图中任意两顶点间都有方向互为相反的两条弧,总弧数为n*(n-1)
  • 稀疏图、稠密图:模糊的概念
  • 权:边或弧上的消耗或距离
  • 网:带有权的图
  • 子图:图A的顶点和边的集合都被包含在图B的顶点和边的集合中