数据结构总结
什么是数据结构?
数据结构是指存在特定相互关系的数据元素的集合。元素之间的相互关系称为数据的逻辑结构,数据元素及元素之间关系的存储称为存储结构或物理结构。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。
数据结构的分类
数据结构的逻辑结构主要分为线性结构与非线性结构两大类。
存储结构主要分为顺序存储、链式存储、索引存储、散列存储
顺序存储:采用一组地址连续的存储单元依次进行存储(例如:数组)。
链式存储:用通过指针链接起来的结点进行存储,结点的地址不要求连续(例如:链表)。
索引存储:建立索引表,通过索引表的索引号还确定结点存储地址。
散列存储:散列存储,又称hash存储,是一种力图将数据元素的存储位置与关键码之间建立确定对应关系的查找技术。散列法存储的基本思想是:由结点的关键码值决定结点的存储地址。除了用来查找之外还可以用来存储。
线性表
线性表的定义
线性结构是一种基本的数据结构,主要用于对客观世界中具有单一前驱和后继的数据关系进行描述。线性结构的特点是数据元素之间呈现一种线性关系,即元素“ 一个接一个地排列 ”。
线性表是最简单、最基本、也是最常用的一种线性结构。通常采用顺序存储和链式存储,主要的基本操作是插入、删除和查找等。
线性表是n(n>=0)个具有相同特性的数据元素的有限序列。非空线性表的特点如下。
(1)存在唯一的一个称作“ 第一个 ”的元素。
(2)存在唯一的一个称作“ 最后一个 ”的元素。
(3)除第一个元素外,序列中的每个元素均只有一个直接前驱。
(4)除最后一个元素外,序列中的每个元素均只有一个直接后继。
线性表的存储结构
线性表的存储结构分为顺序存储和链式存储。
线性表的顺序存储是指用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻,如图:
线性表采用顺序存储结构的优点是可以随机存取表中的元素,缺点是插入和删除操作需要移动元素。
线性表的链式存储是用结点来存储数据元素,基本的结点结构如下所示:
其中,数据域用于存储数据元素的值,指针域则存储当前元素的直接前驱和直接后继信息,指针域中的信息称为指针(或链)。
存储各数据元素的结点的地址并不要求是连续的,因此存储数据元素的同时必须存储元素之间的逻辑关系。
另外,结点空间只有在需要的时候才申请,无需事先分配。
结点之间通过指针域构成一个链表,若结点中只有一个指针域,则称为线性链表(或单链表),如图所示:
在链表的存储结构中,只需要一个指针(称为头指针,如上图中的 head)指向第一个结点,就可以顺序访问到表中的任一个元素。
在链式存储结构下进行插入和删除,其实质都是对相关指针的修改。
线性表采用链表作为存储结构时,不能对数据元素进行随机访问(需要对数据元素进行遍历),但是插入和删除操作不需要移动元素。
根据结点中指针域的设置方式,还有其他几种链表结构。
双向链表。每个结点包含两个指针,分别指出当前结点元素的直接前驱和直接后继。其特点是可从表中任意的元素结点出发,从两个方向遍历链表。
循环链表。在单项链表(或双向链表的基础上),令表尾结点的指针指向表中的第一个结点,构成循环链表。 其特点是可以从表中任意结点开始遍历整个链表。
静态链表。借助数组来描述线性表的链式存储结构。用数组元素的下标表表示元素所在结点的指针。
栈
栈的定义
栈是只能通过访问它的一端来实现数据存储和检索的一种线性数据结构。也就是说,栈是按“ 后进先出 ”的规则进行操作。因此,栈又称为后进先出(Last In First Out,LIFO)的线性表。
在栈中进行插入和删除操作的一端称为栈顶(top),相应地,另一端称为栈底(bottom)。不含数据元素的栈称为空栈。
栈的基本运算
① 初始化栈 InitStack(S):创建一个空栈S。
② 判栈空 StackEmpty(S):当栈 S 为空时返回“ 真 ”值,否则返回“ 假 ”值。
③ 入栈 Push(S,x):将元素 x 加入栈顶,并更新栈顶指针。
④ 出栈 Pop(S):将栈顶元素从栈中删除,并更新栈顶指针。若需要得到栈顶元素的值,可将 Pop(S)定义为一个返回栈顶元素值的函数。
⑤ 读栈顶元素 Top(S):返回栈顶元素的值,但不修改栈顶指针。
栈的存储结构
顺序存储(顺序栈):栈的顺序存储是指用一组地址连续的存储单元依次存储自栈顶到栈底的数据元素,同时附设指针 top 指示栈顶元素的位置。在该存储方式下,需要预先定义(或申请)栈的存储空间,栈空间的容量是有限的。因此,在顺序栈中,当一个元素入栈时,需要判断是否栈满(栈空间中没有空闲单元),若栈满,则元素入栈发生上溢现象。
链式存储(链栈):栈的链式存储是指用链表存储栈中的数据元素,它解决了可能发生上溢的不足。由于栈中元素的插入和删除仅在栈顶一端进行,因此不必设置头结点,链表的头指针就是栈顶指针。
栈的应用
栈的典型应用包括表达式求值、括号匹配等,在计算机语言的实现以及递归过程转变为非递归过程的处理中,栈有重要作用
队列
队列的定义
队列式一种先进先出(First In First Out,FIFO)的线性表,它只允许在表的一端插入元素,而在表的另一端删除元素。在队列中,允许插入元素的一端称为队尾(rear),允许删除元素的一端称为队头(front)。
队列的基本运算
① 初始化队 InitQueue(Q):创建一个空的队列 Q。
② 判队空 Empty(Q):当队列为空时返回“ 真 ”值,否则返回“ 假 ”值。
③ 入队 EnQueue(Q,x):将元素 x 加入到队列Q的队尾,并更新队尾指针。
④ 出队 DeQueue(Q):将队头元素从队列Q中删除,并更新队头指针。
⑤ 读取队头元素FrontQue(Q):返回队头元素的值,但不更新队头指针。
队列的存储结构
顺序存储:队列的顺序存储结构又称为顺序队列,它也是利用一组地址连续的存储单元存放队列中的元素。由于队列中元素的插入和删除限定在表的两端进行,因此设置队头和队尾指针,分别指示出当前的队首元素和队尾元素。
在顺序队列中,为了降低运算的复杂度,元素入队时只修改队尾指针,元素出队时只修改队头指针。下面设顺序队列 Q 的容量为 6,其队头指针 为 front,队尾指针为 rear,头、尾指针和队列中的元素之间的关系如图:
由于顺序队列的存储空间是提前设定的,所以队尾指针会有一个上限值,当队尾指针达到该上限时,就不能只通过修改队尾指针来实现新元素的入队操作了。此时,可通过除取余运算顺序队列假想成一个环状结构,称之为循环队列。
设置循环队列Q的容量 MAXSIZE,初始时队列为空,且Q.rear 和 Q.front 都等于 0。
元素入队时,修改队尾指针 Q.rear = (Q.rear+1)%MAXSIZE。
元素出队时,修改队头指针 Q.front = (Q.front+1)%MAXSIZE。
在队列空和队列满的情况下,循环队列的队头、队尾指针指向的位置是相同的,此时仅仅根据 Q.rear 和 Q.front之间的关系无法判定队列的状态。
为了区别队空和队满的情况,可采用以下两种处理方式:
其一,是设置一个标志位,以区别头、尾指针的值相同时队列式空还是满;
其二,是牺牲一个存储单元,约定以“ 队列的尾指针所指位置的下一个位置是队头指针 ”表示队列满,而头、尾指针的值相同时表示队列为空。
链式存储: 队列的链式存储也称为链队列。这里为了便于操作,给链队列添加一个头结点,并令头指针指向头结点。因此,队列为空的判定条件是:头指针和尾指针的值相同,且均指向头结点。
队列的应用
队列结构常用于处理需要排队的场合,如操作系统中处理打印任务的打印队列、离散事件的计算机模拟等。
串
串的定义
串(字符串)是一种特殊的线性表,其数据元素为字符。计算机中非数值问题处理的对象经常是字符串数据。串是仅由字符构成的有限序列,是取值范围受限的线性表。一般记为 S=‘a1a2...an’,其中 S 是串名,单引号括起来的字符序列是串值。
串的几个基本概念
空串:长度为零的串,空串不包含任何字符。
空格串:由一个或多个空格组成的串。虽然空格是一个空白字符,但它也是一个字符,计算串长度时要将其计算在内。
子串:由串中任意长度的连续字符构成的序列称为子串。含有子串的串称为主串。子串在主串中的位置是指子串首次出现时,该子串的第一个字符在主串中的位置。空串是任意串的子串。
串相等:指两个串长度相等且对应位置上的字符也相同。
串比较:两个串比较大小时以字符的ASCII码值(或其他字符编码集合)作为依据。比较操作从两个串的第一个字符开始进行,字符的码值大者所在的串为大;若其中一个串先结束,则以串长较大者为大。
串的基本操作
① 赋值操作 StrAssign(s,t):将串 s 的值赋给串 t。
② 联接操作 Concat(s,t):将串 t 接续在 s 的尾部,形成一个新串。
③ 求串长 StrLength(s):返回串 s 的长度。
④ 串比较 StrCompare(s,t):比较两个串的大小。返回值 -1、0 和 1 分别表示 s<t、s=t 和 s>t 三种情况。
⑤ 求子串 SubString(s,start,len):返回串 s 中从 start 开始的、长度为 len 的字符串序列。
串的存储结构
顺序存储:串的顺序存储结构是指用一组地址连续的存储单元来存储串值的字符序列。由于串中的元素为字符,所以可通过程序语言提供的字符数组定义串的存储空间,也可以根据串长的需要动态申请字符串的空间。
链式存储:当链表存储串中的字符时,每个结点中可以存储一个字符,也可以存储多个字符,此时要考虑存储密度问题。在链式存储结构中,结点大小的选择和顺序存储方法中数组空间大小的选择一样重要,它直接影响对串的处理效率。
串的模式匹配
子串的定位操作通常称为串的模式匹配,它是各种串处理系统中最重要的运算之一。子串也称为模式串。
(1)朴素的模式匹配算法:
该算法也称为布鲁特-福斯算法,其基本思想是从主串的第一个字符起与模式串的第一个字符比较,若相等,则继续逐对字符串后续的比较,否则从主串第二个字符起与模式串的第一个字符重新比较,直至模式串中每个字符依次和主串中一个连续的字符序列相等时为止,此时称为匹配成功。如果不能在主串中找到与模式串相同的子串,则匹配失败。
(2)改进的模式匹配算法:
改进的模式匹配算法又称为 KMP 算法,其改进之处在于:每当匹配过程中出现相比较的字符不相等时,不需要回溯主串的字符串位置指针,而是利用已经得到的“ 部分匹配 ”结果,将模式串向右“ 滑动 ”尽可能远的距离,再继续进行比较。
设模式串为“ P0... Pm-1 ”,KMP 匹配算法的思想是:当模式串中的字符 Pj 与主串中相应的字符 Si不相等时,因其前 j个字符(“P0...Pj-1”)已经获得了匹配,所以若模式串中的“P0...Pk-1”与“Pj-k...Pj-1”相同,这时可令Pk与Si进行比较,从而是 i 无需回退。
在 KMP 算法中,依据模式串的 next 函数值实现了子串的滑动。若令 next[j] = k,则 next[j] 表示当模式串中的Pj 与主串中相应字符不相等时,令模式串的 Pk 与主串的相应字符进行比较。
next 函数的定义如下:
树
树的定义
树结构是一种非常重要的非线性结构,该结构中一个数据元素可以有两个或两个以上的直接后继元素,树可以用来描述客观世界中广泛存在的层次结构关系。
树是 n(n≥0)个结点的有限集合,当 n=0 时称为空树。在任一非空树(n>0)中,有且仅有一个称为根的结点;其余结点可分为 m(m≥0)个互不相交的有限集 T1,T2,...,Tm,其中每个 Ti又都是一棵树,并且称为根结点的子树。
树的定义是递归的,它表明了树本身的固有特性,也就是一棵树由若干棵子树构成,而子树又由更小的子树构成。
树的基本概念
(1)双亲、孩子和兄弟:结点的子树的根称为该结点的孩子;相应地,该结点称为其子结点的双亲。具有相同双亲的结点互为兄弟。
(2)结点的度:一个结点的子树的个数记为该结点的度。
(3)叶子结点:也称为终端结点,指度为 0 的结点。
(4)内部结点:度不为 0 的结点称为分支结点或非终端结点。出根结点之外,分支结点称为内部结点。
(5)结点的层次:根为第一层,根的孩子为第二层,以此类推,若某结点在第 i 层,则其孩子结点在第 i+1 层。
(6)树的高度:一棵树的最大层次树记为树的高度(或深度)。
(7)有序(无序)树:若树中结点的各子树看成是从左到右具有次序的,即不能交换,则称该树为有序树,否则称为无序树。
二叉树
二叉树的定义
二叉树是 n (n≥0)个结点的有限集合,它或者是空树(n = 0),或者是由一个根结点及两棵不相交的且分别成为左、右子树的二叉树所组成。
树和二叉树之间最主要的区别是:二叉树结点的子树要区分左子树和右子树,即使在结点只有一颗子树的情况下,也要明确指出该子树是左子树还是右子树。另外,二叉树结点最大度为2,而树中不限制结点的度数。
二叉树的性质
(1)二叉树第 i 层(i≥1)上至多有 2^{i-1}个结点。
(2)高度为 k 的二叉树至多有 2^{k}-1个结点(k≥1)。
(3)对任何一棵二叉树,若其终端结点数为 n_{0},度为2的结点数 为n_{2},则n_{0}=n_{2}+1。
(4)具有 n 个结点的完全二叉树的深度为log₂n+1。
若深度为k的二叉树有 个结点,则称其为满二叉树。对满二叉树的结点进行连续编号:约定编号从根结点起,自上而下、自左而右依次进行。深度为k、有n 个结点的二叉树,当且仅当其每一个结点都与深度为 k的满二叉树中编号从 1 至 n 的结点一一对应时,称之为完全二叉树。满二叉树和完全二叉树的示意图如图所示
二叉树的存储结构
顺序存储:用一组地址连续的存储单元存储二叉树的结点,必须把结点排成一个适当的线性序列,并且结点在这个序列中的相互位置能反映出结点之间的逻辑关系。
对于深度为 k 的完全二叉树,除第 k 层外,其余各层中含有最大的结点数,即每一层的结点数恰为其上一层结点数的两倍,由此从一个结点的编号可推知其双亲、左孩子和右孩子的编号。
假设有编号为 i 的结点,则有:
若 i=1 ,该结点为根结点,无双亲;若 i>1 ,该结点的双亲结点为i/2(取整数)。
若 2i≤ n,则该结点的左孩子编号为2i,否则无左孩子。
若 2i+1 ≤ n,则该结点的右孩子编号为 2i+1,否则无右孩子。
二叉树的顺序存储结构如图所示
完全二叉树采用顺序存储结构既简单又节省空间,对于一般的二叉树,则不宜采用顺序存储结构。因为一般的二叉树也必须按照完全二叉树的形式存储,也就是要添上一些实际并不存在的“虚结点”,这将造成空间的浪费。
链式存储:由于二叉树的结点中包含有数据元素、左子树的根、右子树的根及双亲等信息,因此可以用三叉链表或二叉链表(即一个结点含有三个指针或两个指针)来存储二叉树,链表的头指针指向二叉树的根结点。
二叉树的遍历
遍历是按某种策略访问树中的每个结点,且仅访问一次的过程。由于二叉树所具有的的递归性质,一棵非空的二叉树可以看作是由根结点、左子树和右子树三部分构成的,因此若能依次遍历这三部分,也就遍历了整棵二叉树。按照先遍历左子树后遍历右子树的约定,根据访问根结点位置的不同,可得到二叉树的先序、中序和后序三种遍历方法。
先序遍历:按照根-左-右的顺序进行遍历。
中序遍历:按照左-根-右的顺序进行遍历。
后序遍历:按照左-右-根的顺序进行遍历。
二叉树的遍历实质上是对一个非线性结构进行线性化的过程,它使得每个结点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。
最优二叉树
最优二叉树的定义
最优二叉树又称为哈夫曼树,是一类带权路径长度最短的树。 路径是从树中一个结点到另一个结点之间的通路,路径上的分支数目称为路径长度。
树的路径长度是从树根到每一个叶子之间的路径长度之和。结点的带权路径长度为从该结点到树根之间的路径长度与该结点带权的乘积。
树的带权路径长度为树中所有叶子结点的带权路径长度之和,记为
其中, n 为带权叶子结点数目,w_{k}为叶子结点的权值,l_{k}为叶子 结点到根的路径长度。
下图所示的具有 4 个叶子结点的二叉树,其中以图(b)所示二叉树带权路径长度最小。
构造最优二叉树的哈夫曼算法如下:
(1)根据给定的 n 个权值{ w1,w2,...,wn },构成 n 棵二叉树的集合 F={T1,T2,...,Tn },其中每棵树Ti中只有一个带权为wi的根结点,其左右子树均空。
(2)在 F 中选取两棵权值最小的树作为左、右子树构造一棵新的二叉树,置新构造二叉树的根结点的权值为其左、右子树根结点的权值之和。
(3)从 F 中删除这两棵树,同时将新得到的二叉树加入到 F 中。
重复 (2)、(3)步,直到 F 中只含一棵树时为止,这棵树便是最优二叉树(哈夫曼树)。
哈夫曼编码
若对每个字符编制相同长度的二进制码,则称为等长编码。例如,英文字符集中的 26个字符采用 5 位二进制串表示,按等长编码格式构造一个字符编码表。发送方按照编码表对信息原文进行编码后送出电文,接收方对接收到的二进制代码按每 5 位一组进行分割,通过查字符的编码表即可得到对应字符,实现译码。
等长编码方案的实现方法比较简单,但对通信中的原文进行编码后,所得电文的码串过长,不利于提高通信效率,因此希望缩短码串的总长度。如果对每个字符设计长度不等的编码,且让电文出现次数较多的采用尽可能短的编码,那么传送的电文码串总长度则可减少。
要设计长度不等的编码,必须满足下面的条件:任一字符的编码都不是另一个字符的编码的前缀,这种编码也称为前缀码。
对给定的字符集 D及字符的使用频率 W,构造其最优前缀码的方法为:以 D作为叶子结点,W 作为叶子结点的权值,构造出一棵最优二叉树,然后将树中每个结点的左分支标上 0,右分支标上 1,则每个叶子结点代表的字符的编码就是从根到叶子的路径上的 0、1组成的串。
利用哈夫曼译码的过程为:从根结点出发,按二进制位串(编码序列)中的 0 和 1 确定是进入左分支还是右分支(当前编码为0进入当前结点的左子树,为1则进入右子树),当到达叶子结点时译出一个字符。若位串未结束,则回溯到根结点接续上述译码过程。
例如,设有字符集{a,b,c,d,e}及对应的权值集合{0.3,0.25,0.15,0.22,0.08},按照构造最优二叉树的哈夫曼算法构建最优二叉树后得到
若编码序列为101110000100,翻译出的字符序列为"edaac"。
二叉排序树
二叉排序树的定义
二叉排序树又称二叉查找树,它或者是一棵空树,或者是具有以下性质的二叉树。
(1)若它的左子树非空,则左子树上所有结点的值均小于根结点的值。
(2〉若它的右子树非空,则右子树上所有结点的值均大于根结点的值。
(3)左、右子树本身是二叉排序树。
如图:
二叉排序树的查找过程
二叉排序树非空时,将给定值与根结点的关键字值相比较,若相等,则查找成功;若不相等,则当根结点的关键字值大于给定值时,下一步到根的左子树中进行查找,否则到根的右子树中进行查找。若查找成功,则查找过程是走了一条从树根到所找到结点的路径;否则,查找过程终止于一棵空的子树。
在二叉树中插入结点
二叉排序树是通过依次输入数据元素并把它们插入到二叉树的适当位置构造起来的,具体的过程是:每读入一个元素,建立一个新结点。若二叉排序树非空,则将新结点的值与根结点的值相比较,如果小于根结点的值,则插入到左子树中,否则插入到右子树中;若二叉排序树为空,则新结点作为二叉排序树的根结点。设关键字序列为{46,25,54,13,29,91},则整个二叉排序树的构造过程如图所示。
在二叉树中删除结点
在二叉排序树中删除一个结点,不能把以该结点为根的子树都删除,只能删除这个结点并仍旧保持二叉排序树的特性。也就是说,在二叉排序树上删除一个结点相当于在有序序列中删除一个元素。删去叶子结点后需要修改其双亲结点已经左右子树的指针保证不破坏整棵树的结构。
平衡二叉树
平衡二叉树又称为AVL树,它或者是一棵空树,或者是具有下列性质的二叉树。它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度之差的绝对值不超过1。若将二叉树结点的平衡因子(Balance Factor,BF)定义为该结点左子树的高度减去其右子树的高度,则平衡二叉树上所有结点的平衡因子只可能是-1、0和1。只要树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
分析二叉排序树的查找过程可知,只有在树的形态比较均匀的情况下,查找效率才能达到最佳。因此,希望在构造二叉排序树的过程中,保持其为一棵平衡二叉树。
使二叉排序树保持平衡的基本思想是:每当在二叉排序树中插入一个结点时,首先检查是否因插入破坏了平衡。若是,则找出其中的最小不平衡二叉树,在保持二叉排序树特性的情况下,调整最小不平衡子树中结点之间的关系,以达到新的平衡。所谓最小不平衡子树,是指离插入结点最近且以平衡因子的绝对值大于1的结点作为根的子树。
平衡二叉树上的插入操作
假设由于在二叉排序树上插入结点而失去平衡的最小子树根结点的指针为a,也就是说,a所指结点是离插入结点最近且平衡因子的绝对值超过1的祖先结点,那么,失去平衡后进行调整的规律可归纳为以下4种情况。
(1) LL型单向右旋平衡处理。如下图所示,由于在*a(即结点A)的左子树的左子树上插入新结点,使*a的平衡因子由1增至2,导致以*a为根的子树失去平衡,因此需进行一次向右的顺时针旋转操作。
(2)RR型单向左旋平衡处理。如下图所示,由于在*a(即结点A)的右子树的右子树上插入新结点,使*a的平衡因子由-1变为-2,导致以*a为根的子树失去平衡,因此需进行一次向左的逆时针旋转操作。
(3)LR型先左后右双向旋转平衡处理。如下图所示,由于在*a(即结点A)的左子树的右子树上插入新结点,使*a的平衡因子由1增至2,导致以*a为根结点的子树失去平衡,因此需进行两次旋转(先左旋后右旋)操作。
(4)RL型先右后左双向旋转平衡处理。如下图所示,由于在*a(即结点A)的右子树的左子树上插入新结点,使*a的平衡因子由-1变为-2,导致以*a为根结点的子树失去平衡,因此需进行两次旋转(先右旋后左旋)操作。
平衡二叉树上的删除操作
在平衡二叉树上进行删除操作比插入操作更复杂。若待删结点的两个子树都不为空,就用该结点左子树上的中序遍历的最后一个结点(或其右子树上的第一个结点)替换该结点,将情况转化为待删除的结点只有一个子树后再进行处理。当一个结点被删除后,从被删结点到树根的路径上所有结点的平衡因子都需要更新。对于每一个位于该路径上的平衡因子为+-2的结点来说,都要进行平衡处理。
参考资料:《软件设计师教程(第5版)》 电子书链接:https://pan.baidu.com/s/1OEQJ6TLd1FzcFT852UBc-A 提取码:7za7
ps:数据结构还有部分内容未总结,后续有时间会把其余内容补充完整
作者:ki16
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。