数据结构学习(考研408)

数据结构学习笔记,若有任何问题欢迎大家评论指出

其他

1.存取包括存和取
2.第i个元素指下标为i-1的位置
3.注意题干,注意题干中的陷阱

开端

有关概念:一个数据元素由若干个数据项组成,数据对象是具有相同性质的的数据元素的集合。 抽象数据元素可以理解为抽象成员,他是没有实际含义的数据元素,当实际使用时才确定他的数据类型
数据结构是相互存在一种或多种特定关系的数据元素的集合(数据之间的关系)
数据结构三要素: 存储结构(顺序,链式,索引,散列) 数据运算(与或非加减乘除等等)
逻辑结构:集合,线性,树形,图或网(主要分为线性和树状,图形) 线性是除了首尾外唯一前驱后继,树是除了根外唯一前驱不限后继数。图不限前驱后继数
逻辑结构+存储结构=物理结构,例:线索二叉树是物理结构
抽象数据类型包含逻辑结构和数据运算,可以定义一个完整的数据结构(实现则要用到存储结构)
是从基本数据类型中抽象出来的,即几种类型混合使用的类型,例如表,队列,堆等等
算法具有的特性:确定性:即运算结果是唯一的,每一步是确定的不是模棱两可的
有穷性,至少一个输出,有效性
最坏情况下某语句的频度指某一语句出现的频率(执行次数最大的时候)
时间和空间复杂度
时间复杂度:T(n) 一般表示程序执行次数T和传入的参数n的关系
一般只关注n的最高阶数,即如果T(n)=6n3+10n2+100000000,一般我们都假设T(n)=O(n3
o(1)<o(log2n)<O(n)<o(nlog2n)<O(n2)<o(n3)<o(2n)<O(n!)<O(nn)
例:传入i=1;i<n i=2i 则i=1,2,4,8,16 所以2x=n,即x=log2n,所以时间复杂度为log2n
空间复杂度:S(n) 一般表示动态容器扩容问题
无论问题的规模怎么变,算法运行所需的内存空间都是固定的常量,则空间复杂度S(n)=O(1),这种情况我们成为“原地工作

线性表

注意:1.头指针不等于头结点,头指针指头结点的指针域,指向第一个结点,
所以设置尾指针的链表指尾结点存在指针域,其值一般设置为NULL
2.带头结点的单链表,头结点不是第一个元素
线性表:是一种最简单的线性结构。相同数据类型的m个数据元素的有限序列,以下都是线性表
顺序表:指用顺序存储(数组)的方式实现的顺序表
位序:数据元素位序是从1开始的,即下标为0的数
特点:不易扩容,易查找,不易插入和删除,为随机存取结构
单链表:每一个结点中包含数据域和指针域,指针域指向下一个结点的地址
每一个结点有自己的地址(包含 一个结点的空间)在这个空间中有两个小块空间,一个空间是存储数据的空间,一个空间是存储指针的空间
特点:易扩容,不易查找,易插入和删除,为非随机存取结构(不能直接找到某个特定的点)
如何在单链表的某一个节点(p)前插入元素呢?(单链表访问不到前一个节点)
删除某个结点:删除结点p,无法使p的前驱指向p的后继,所以直接使p等于p的后继即可
(创建指针L,L=p;p=p.next;free(L)😉
逆置某个链表:使用头插法从链表的头节点开始创建一个新链表,(每一个结点都使用前插操作插入在前面)
结点的插入后插操作:可以将新的节点(s)插入到p的后面(即p.next=s; s.next=p.next)
前插操作:可以将新的节点(s)插入到p的前面,将s插入p后面,然后交换s,p的数据即可
头插法和尾插法建立链表:头插法用的前插操作,
尾插法就是增加一个总是指向链尾结点的指针,将新节点加入链尾的后面,新结点成为链尾节点
其他链表
静态链表:使用连续的一大片内存存储链表,即使用数组实现链表,每一个结点存储数据和下一个结点在数组中的下标
双链表:即每个节点增加一个指向前驱的指针,头结点和尾结点不是双向的,所以双链表不是循环的
循环链表:最后一个结点的后继不是NULL,而是指向头结点,循环链表为空则头结点的前驱后继都指向自己
typedef struct{int value;}List [10]; List a; 即a是一个包含10个结构的结构数组

//使用尾插法建立单链表
LinearList* createlist(LinearList* &h) {
 h= (LinearList*)malloc(sizeof(LinearList));//h为头节点
 int x=1;
 LinearList* p,*e;//p为新添节点,e为尾结点
 e = h;   //e的初始值为头节点,因为一开始链表只有头节点,所以头节点为尾节点
 while (x != 0) {//输入-1结束循环
  scanf("%d", &x);
  p = (LinearList*)malloc(sizeof(LinearList));
  p->data = x;
  e->next = p;
  e = p;}
 e->next = NULL;//尾指针设置为NULL
 return h;}
 2------------------------------------------------------------------------------------
//就地逆置带头结点的单链表 将头结点后面的结点依次插入到头节点后面(后插操作,也可以使用前插操作将后面的结点依次往第一个结点前插)
LinearList* reverse(LinearList* &l) {
 LinearList* p, * r;
 p = l->next;   //p是即将要插入在头节点后的结点
 l->next = NULL;   //将头节点摘下,并且此时的头节点就是尾结点,所以next域指向null
 while (p!=NULL) {//p是指针,执行p=l.next 再令p=l.next.next不会改变l.next的地址
  r = p->next;  //存储下一个结点的地址   
  p->next = l->next; //使新插入的结点的next域指向上一次插入的结点 
  l->next = p;  //将新结点插入到头节点后面
  p = r;    //令p等于下一个结点继续循环
 }
 return l;}

栈和队列

(操作受限的线性表)

栈(stack):只能在栈头进行操作,先进后出,”堆“
实现栈(顺序存储结构):定义一个结构,包含一个存储栈中元素的静态数组(data)和表示栈顶的指针(top)(栈顶元素存储在数组中的下标,操作数据是从数组的尾部操作的)
初始化:设置top=-1 表示栈空 栈长为栈顶下标+1
链式存储结构:和单链表基本一样,只能在链表头操作结点
共享栈:顺序存储方式实现的栈,结构体包含两个指向栈顶的指针,一个从数组头开始,一个从尾开始,即从数组头定义一个栈,从数组尾也定义一个栈,增加空间的利用率,
—在栈中,给你n个数,有多少种出栈的顺序? 1/n+1 * Cn(上)2n(下)

队列

队列:只允许在队尾进行插入,在队头进行删除,先进先出(队尾进,队头出)
静态队列(顺序存储结构):结构体包含一个数组和队头(top)及队尾指针(end)
初始化:将top,end设置为0(表头指针指向队头结点,表尾指针指向尾结点下一个位置)
循环队列:即将数组的线性空间当作环状空间使用。
//判断循环队列是否满有三种方式,常见第一种
1.数组中的一个位置不存储数据,以保证在队满时,队尾指针在队头指针的后面
(q->end + 1) % n = = q->top;
2.增加一个用于计算当前队列中数据个数的变量
3.增加一个布尔类型的变量(tag),当上一次操作是入队时,令tag=1,
则当tag=1且q->end == q->top时,队列满,(因为入队操作不会导致队空)

 q->end = (q->end + 1) % n;//插入元素
 q->top = (q->top + 1) % n;//删除元素
 lengh = (q->end + n - q->top) % n;//计算当前队列的长度
 q->end == q->top; 	 //队空

链式存储的队列:是一个有队头指针和队尾指针的单链表(一般是带头结点的)。定义一个结构表示单链表,再定义一个结构表示队列,结构中有两个指针,分别表示链表头和尾
所以一个链队的基本组成是头结点和队头及队尾指针
双端队列:允许在两端进行删除和添加操作(双端序列没有先进先出?)
输入受限的双端队列就是只能在一端输入,但可以两端输出

队列和栈的应用

栈的应用:
栈与递归:递归的深度越大,越可能导致栈溢出(超出栈的容量),
因为系统会开辟一个栈来存储每一层递归的返回点,局部变量,传入的实参等数据
递归就是递推式,例f(n)=f(n-1)
递归中,递归体内的变量(包括参数)每一层的值都会压入栈中,在返回时依次弹出
分析递归的时间复杂度按进出栈的次数和分析。循环的效率远高于递归
进栈n次出栈n次,时间复杂度为2n。。空间复杂度计算递归的深度?
递归转为非递归1.用循环2.用栈
括号匹配判断一个括号序列是否合法,即每一个左括号都有一个右括号对应,用栈实现,每一个左括号入栈,出栈时匹配右括号
中缀表达式就是平时的写法
前缀表达式(波兰表达式):a+b写成+ab ,a+b-c写成-+abc或+a-bc(从右边开始计算)
后缀表达式(重点)(逆波兰):即把运算符放在操作数后面。对于一个计算,先提出操作数,再将操作符放后面即可 例a+b写成ab+ 则再以ab+为一个操作数进行运算,例:a+b-c写成ab+c-
也可以写成abc-+ (从右边开始计算)
在把中缀改为后缀时应该按左优先原则(使运算结果唯一),即左边的运算符优先执行,这样得出的结果中的运算符的顺序和你先定的计算顺序一致
例:a/b+cd 运算顺序应该定义为/ * + (不定义为 * /+),则改为后缀为 ab/cd * +
实现将后缀转换为中缀(计算后缀表达式的值)
从左向右扫描字符,如果是操作数则存储在栈中,如果是运算符则移出栈中的两个元素进行运算,将运算结果放回栈中(其结果当作一个数)先出栈的是右操作数,例a-b写为ab- 则a先入栈,b先出栈
前缀表达式的计算只要改为从右扫描字符即可
-实现将中缀转换为后缀
A+B-C * D/E+F 写成 AB+CD
E/-F+
用两个栈来实现,一个栈存储结果,一个栈用于存储运算符
从左到右处理各个元素,直到末尾。可能遇到三种情况(左优先,先算左边):
①遇到操作数。直接加入后缀表达式。
②遇到界限符。遇到“(”直接将(入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。括号不需要加入后缀表达式中
③遇到运算符。如果栈中已经有运算符,则依次弹出优先级比遇到的运算符高或相等的所有运算符(例:栈从上至下有 * - ,遇到+,则弹出 * -,遇到/则弹出*),(括号的优先级比所有运算符高

然后把遇到的运算符入栈
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达(即栈底元素是后缀表达式的第一个元素)
队列的应用
树的层次遍历(图的广度优先搜索)。操作系统中的先来先服务策略(解决多用户竞争资源)。解决主机与外部设备速度不匹配的问题等等

矩阵的压缩存储

c语言是按行主序
普通矩阵用二维数组存储即可,矩阵的压缩存储即将矩阵存储在一维数组中,考察存储和查找
对称矩阵,对称矩阵即中斜轴对称的矩阵,三角矩阵则多用一个位置存储那个常量即可
—存储:用一维数组按行或列存储轴和上下任一区的元素即可,A表示一维数组,a表示矩阵
若按行存储,则A[0]存储a[1] [1],A[1]存储a[2] [1],A[2]存储a[2] [2]…
—查找:如果按行存储,则i>j区(即下区)第a[i][j]个元素对应数组中的下标为i(i-1)/2+j-1
a[i][j]在A[n]中对应的下标为这个元素前面有多少个元素,i(i-1)/2+j-1,i(i-1)/2是前i-1行元素的个数(等差数列求和),j-1是元素所在行在该元素前面的元素的个数 即 a[i][j]=A[i(i-1)/2+j-1] (下标从0开始) 访问上区的元素只要把ij互换即可
上三角矩阵的查询和按列存储的查询一样 a[i][j]=A[(i-1)(2n-i+2)/2+j-i] (下标从0开始)
带状矩阵(三对角矩阵):中斜轴除对角两个元素外每个元素的上下左右都有一个非0元素,其他元素都是0(只有中间三条斜轴有数据)
—存储:和对称矩阵一样,按行或列存储都可
—查找:a[i][j]=A[2i+j-3](下标从0开始)
稀疏矩阵:即非0元素少于为0的元素,
—存储方法1:用一个结构存储非0元素的值和其地址(行值和列值)(或者用二维数组存储)
方法2:十字链表法,即用两个数组,一个数组存储行的非0元素,一个数组存储列的非0元素。
十字链表是由一个横向和竖向的数组构成,数组中的每个元素都是一个单链表,单链表中的每一个结点包括三个数据和两个指针,三个数据指行值和列值和本身的值,两个指针分别为指向同行的下一个元素及同列的下一个元素,因为每个结点都可以有上下左右的指针,所以称为十字链表
(考研不考)广义表:扩展线性链表
广义表就是广义的线性表,即表中的每个元素都可以是一个广义表,广义表中的单个元素称为原子(非子表)
例:表a=(1,3(4,1),(1,(4,6))),表的深度指子表括号数+1,即a的深度为3
广义表的每个结构由三个元素组成(指向头和尾的指针,和一个标志位(是为原子还是子表)),
子表中的每个元素用含三个元素的结构构成(值和指向下一个元素的指针和(标志位?))(见222)

串也是线性结构,是特殊的线性表
子串:串中任意个连续的字符串组成的子序列(可以为0)
串的位置:第一个字符的位置(位序是从1开始)
每个英文占一个字节
串的链式存储:一个结点存储一个指针和一个字符数组(数组长度一般是4)
这样的做原因是提高存储密度,因为一个指针占4B而一个字符占1B,存储密度低
子串的定位操作称为串的模式匹配(KMP算法就是串的模式匹配)

KMP算法

目的:传入一个长度为n的子串,在主串中寻找是否有和子串相同的序列
brute-force算法(常规字符匹配算法):从第一个相等的字符开始匹配,若失败则从第二个相等的字符开始匹配
缺点:是当出现连续比较相等但最后一个值不相等时会浪费时间,且程序每一次比较失败会从头开始匹配
时间复杂度是O(mn)
kmp算法有时比较次数也会多于上述算法,在模式串多次右滑,仍然比较不相等时发生
KMP算法的思路:当匹配不相等时,子串应该回到哪开始继续比较,先将这个算出来放入一个数组中,然后当比较不相等时直接回到应该回到的位置(能节约时间的位置)
以主串A为abcabcac子串B为abcabd为例:当指针a和b为6时比较失败(a是主串指针,b是子串指针,表示下一次是A[a-1]与B[b-1]比较)即A[a-1]不等于d时,则令b等于3继续匹配(所以next[5]=3)
时间复杂度是O(m+n)主串和子串的长度和
在这里插入图片描述
在这里插入图片描述

求next数组:next[j]=s s=前后缀相等且最长的子串的长度+1 其他情况s=1 (前后缀指从前往后从前面取两个数和从后面取两个数比较,然后从前面取三个数和从后面取三个数比较)
书上是第一个数的next值为-1 .其他情况为0(一个数匹配的和没有数匹配的都是0))
next[1]=0 next[2]=1(数组的第一位数恒等于0,第二位恒等于1),
例:一,23325 next[1]=0 next[2]=1 next[3]=1 next[4]=1 next[5]=1
二, 23235 next[1]=0 next[2]=1 next[3]=1 next[4]=2 next[5]=3
在二中 next[4]=2取的是第一个数和第三个数(前后缀等于2),next[5]=3取23和23
优化版的KMP算法:就是对得到的next数组进行优化为nextval[]数组
如果第n个元素匹配失败,且前面有和这个数相等的数,则这个数的下标等于前个数的下标
从next获得nextval[]数组,从第一个数开始,若后面有和前面一样的数,且在next数组中的值等于前面那个数的下标,则将后面数的值定为前面那个数的值
例:23325优化为nextval[1]=0 nextval[2]=1 nextval[3]=1 nextval[4]=0 nextval[5]=1
二。 23235 nextval[1]=0 nextval[2]=1 nextval[3]=0 nextval[4]=1 nextval[5]=3
在二中,nextval[3]=0是因为子串B中,B[3]=B[1],且next[3]=1所以令nextval[3]=next[1]
KMP算法的代码实现思路:问题1.主串指针a什么时候后移?答:当b==0 或A[a-1]==B[b-1] 时a++
(因为在next数组中第一个数始终为0,所以当b=0时,则说明需要重新开始匹配了,即a++,b=1)
其他情况令b=next[b]即可(next数组中位序从1开始,字符串的存储位序从0开始(普遍定义))

相关概念术语

森林:大于等于0个树的集合
:树中一个结点的子结点的个数称为该结点的度,树中最大的度为该树的度
叶子结点:度=0的结点,又称为终端结点。
分支结点:度大于0的结点
前驱结点:指按某种遍历方法,先遍历b再遍历a,则b为a的前驱结点,后继结点类推
:父结点和子结点的连线(关系)称为边,n个结点的树有n-1条边
树的高度:高度=层数=深度 树的深度不包括根,即一个结点的树深度为0,两个结点的树深度为1
兄弟结点:同一层且有同一个父亲的结点互为兄弟结点,
路径长度:两个结点之间的路径上所经过的边的个数
结点的带权路径长度:=结点的权值 * 其深度 (根结点的深度为0)
树的带权路径长度(WPL)是所有叶结点的带权路径长度之和
树的性质:
树的所有结点数=所有结点的度数和+1(即n1+2n2+3n3…)(n2表示度为2的结点的个数)

二叉树

二叉树不是一种特殊的树,度为2的有序树不是一颗二叉树
二叉树的定义:可以为空,度为2,有序树(左右结点是有序的)
二叉树的性质:1.非空二叉树的叶子结点数等于度为2的结点数+1
特殊的二叉树
-----满二叉树:除叶子结点外的结点的度数都为2
性质:1.结点数为2^n - 1 (n为树的深度)
2.编号为i的结点(左子树比右子树小),其父结点的编号为[i/2]向左取整,左孩子为2i,右孩子为2i+1
----完全二叉树:只有当一颗树按满二叉树的编号方法编号时(从左一层一层的编号),则这个树为完全二叉树。即最后一层可能不能像二叉树一样放满
性质:1.若有度为1的结点,则只有可能有1个,且该结点只有左孩子结点
2.若结点数为m则树的高度为log2(n+1)或log2n +1
----二叉排序树(二叉搜索树):左孩子结点小于父结点,右孩子结点大于父结点
---- 平衡二叉树:每一个结点下的左右子树的深度差不超过1
二叉树的存储结构
顺序存储结构: 适用于完全二叉树和满二叉树,存储其他二叉树会浪费很多空间
一个完全二叉树的左结点为2i,右结点为2i+1,(从上-下,左-右编号),按编号存储在对应数组的下标中
而非完全二叉树只要将非完全二叉树补成完全二叉树即可,空位存储0
链式存储结构:一般用两个指针和一个数据域的结构体表示一个结点,指针分别表示左和右孩子结点。也可以增加一个指针指向父结点,这时称为三叉链表
二叉链表中非空指针的个数=边数=n-1 所以空指针的个数=2n-(n-1)=n+1

二叉树的遍历

相关概念:中序后继:即按中序遍历方法,遍历A后应该再遍历B,则B是A的中序遍历后继
假设我们定义一个规则(先遍历左子树,再遍历右子树,即先把根的左边遍历完再遍历右边)则有三种递归遍历方法:1.左右根 2.根左右 3.左根右 (所谓先序遍历指的是先遍历根)
1.先序遍历(根左右):从上至下,先遍历根,然后先序遍历左子树,再先序遍历右子树
对于下面图片中的例子,先序遍历的结果是A BDE CFG 先A再B,对于B,先B再DE,所以是ABDE
2.中序遍历(左根右):从上至下,先中序遍历左子树,然后遍历根,再中序遍历右子树
下图中序遍历的结果是 DBE A FCG
2.后序遍历类推,下图后序遍历的结果是 DEB FGC A
在这里插入图片描述
非递归遍历则使用栈实现,先序按中序类推即可
用栈实现中序遍历:1.初始时将根结点及根的所有左侧结点放入栈中
2.然后出栈一次
3.若有右子结点,则将右子结点作为根结点重复1,2,3若没有右子结点,则重复2.3
以上图的树为例,依次入栈ABD,然后出D,B,入E,出E,A,入C,F,出F,C,入G,出G
后序:思路:第一步和中序一样,先找到第一个访问的结点,即最左端的结点,中途结点依次入栈
第二步:访问栈顶结点,若有右子树且从未被访问过,则重复第一步,若无右子树则出栈并访问,且该结点就是最近被访问的结点,然后继续访问栈顶元素,如此循环
以上图为例,依次入栈ABD,访D,出D,访B,入E,访E,出E,访B,出B。。。
层次遍历用队列实现即可:先入根,若队列不为空则一直循环,出队,入根左,入根右。(左到右 )

先序和中序的线索二叉树不需要栈的支持,而后序线索二叉树依然需要(不借助栈无法完成遍历)
//用栈实现非递归遍历树(中序)先序遍历只要改成先访问结点再入栈即可
void InOrder2 (BiTree T) {
InitStack(S); BiTree p=T;	//初始化栈S: P是遍历指针
while(p||!IsEmpty(S)) {	//栈不空或p不空时循环
if(p) {				//一路向左
Push(S,p) ;			//当前结点入栈
p=p->lchild;			//左孩子不空,一直向左走
}else {				//出栈,并转向出栈结点的右子树
Pop(S,p);visit(p); 		//栈顶元素出栈, 访问出栈结点
p=p->rchild;		//向右子树走,p赋值为当前结点的右孩子/ /返回while循环继续进入if-else语句
}}}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//用栈实现非递归遍历树(后序)
void InOrder2 (BiTree T) {
InitStack(S); BiTree p=T; 	//初始化栈S: P是遍历指针
r=NULL			//r是指向最近被访问的结点
while(p||!IsEmpty(S)) { //栈不空或p不空时循环
if(p) {  		  //一路向左
Push(S,p) ;   	//当前结点入栈
p=p->lchild;  	 //左孩子不空,一直向左走
}else {   		 //出栈,并转向出栈结点的右子树
GetTop(S,p);		//访问栈顶结点,并使p指向这个结点(不出栈)
if(p->rchild&&p->rchild!=r){	//若右子树存在且从未被访问过
p=p->lchild;			//遍历右子树
}else{
Pop(S,p);visit(p);   //栈顶元素出栈, 访问出栈结点
r=p;p=NULL;		//记录最近访问的结点,并重置指针p
}}}}

层次遍历,即从上至下一层一层的遍历,使用队列实现
1.将根入队,然后出队,2.将根的左,右子结点入队(先左后右)
3.出队一个元素,4.将出队的结点的左右子结点入对
1重复操作至队空为止
由遍历方法构造二叉树
先(或后或层)序遍历加中序遍历即可构造确定的二叉树(即唯一的一颗树)(即先根后先左或先右)
线索二叉树:若无左子1树,则将左指针指向其前驱结点,若无右子树,则将右指针指向其后驱结点
并增加两个标识位用于区分其指针是指向的子结点还是前驱后继
二叉树的线索化:即遍历一次树,实现将二叉树变为线索二叉树
中序线索二叉树:空指针所指向的前驱后继是通过中序遍历得到的序列,该结点的前驱后继
线索二叉树上线索数为n+1
关于遍历的解题技巧:不需要把完整的树构出来,根据以下性质从排除选项的出答案即可
1.中序遍历序列中,根结点左边是左子树中序序列,右边是右子树中序序列,
第一个数是最左端的结点,最后一个数是最右端的结点
2.先序序列中,第二个数是根的左子结点(若存在),最后一个数是最右端的结点
3.后序序列中,倒数第二个数是根的右子结点(若存在),第一个数是最左端的结点
4.前序和后序遍历序列中,顺序相反的结点可以确定祖孙关系,例:eabc,ebca,则a是bc的祖先1
遍历的应用
1.树的高度的计算:用层次遍历,设置两个变量,一个指向当前层最右的结点(r),一个记录当前正在访问的结点的层数(b) 初始r=b=0,根入队,end=根,r=end,出队,入其左右子结点,end随着加
判断出队结点==r,若是,则b++,r=end,若不是,继续出队。循环这个过程即可
(在层序遍历中,出队的结点就是正在被访问的结点。出队后将其左右子结点入队,并判断正在访问的结点是不是最右结点,若是,则b++;且r=end(下一层最右结点))
(在层次遍历中的队列,一般队头指针top指向第一个结点前一个位置,队尾指针end指向最后一个结点)
2.求树的最大宽度:设置一个结构体,里面包含一个表示队列的数组及及指向队头队尾的指针,还包括一个数组A,表示队列中对应位置的结点所在的层数
在层次遍历中,将队列中每个结点对应的层数存储在A中,然后利用A遍历队列找出最大宽度(统计队列中第i层的结点数,统计到第j个值时,A[j]!=i,则统计第A[j]层的结点个数(I=A[j]),直到j指向队列的末尾)
先记录出队结点的层数k,则在入队时,入队结点的层数为k+1
3.判断一颗树是否为完全二叉树:将二叉树的所有结点层次遍历后加入数组中(空节点也遍历)。
在数组(链表也行)中,若空结点后有结点,则不是完全二叉树

树与森林

森林中所有树都互为兄弟关系
树的存储方法
1.孩子存储法:对3做出一个修改,A中包含结点的值和一个单链表(单链表存储每个结点的孩子结点)
2.左孩子右兄弟存储法:(可用于树与二叉树的转换)
即用链表存储,每一个结点包含三个元素(数据,指向左子结点的左指针,指向右兄弟结点的右指针
3.双亲存储法:即用两个结构,一个结构A包含结点的值和其父节点的下标(根节点的下标用-1表示),另一个结构B包含一个A类型的数组和表示该数组中数据的个数的变量
树,二叉树和森林的互换
树–>二叉树:用左孩子右兄弟存储法即可(树转化成的二叉树右子树必为空)
森林–>二叉树:将每一棵树转为二叉树,然后每棵二叉树的根依次作为上一棵二叉树的右子树
二叉树–>森林:(从二叉树到森林逆推即可)二叉树右根断开,根的右节点为根的兄弟结点
森林和二叉树的遍历方法相同,遍历次序相同
树的先根遍历和二叉树的先序遍历相同,后根遍历和中序遍历相同

树的应用

(考研不考)并查集:给一个集合或者多个集合,将其用双亲存储法存储
即每个集合表示一颗树,令集合中第一个元素为根,则其他元素就是它的子结点
如果为一个集合我们可以认为他是一个数组或者里面所有值都是没有子结点的树

二叉排序树:二叉查找树,树中所有左子树上所有的结点都小于根结点,右子树所有结点大于根结点
二叉排序树中所有结点的值不同
二叉排序树的中序遍历序列是递增有序的序列
创建二叉排序树:将结点按规则插入到末尾即可(即新加入的结点一定是叶子结点,且在查找新插入的结点时一定是在最后找到的)
二叉树的删除:若被删除的结点是叶子结点,直接删除即可,若该结点有一个子结点,让子结点替换其即可 ,若被删除的结点A有左右子树,则让其中序遍历的后继B代替A,则B的位置空了,继续按规则补充B的位置
查找成功的平均查找长度=每一层的结点数*层数的和/结点总数
查找失败的平均查找长度=每个结点的深度乘其空指针的数量的和/树的空指针的数量
平衡二叉树(AVL树):左子树的高度-右子树的高度<=1 查找平均时间复杂度为log2^n
创建平衡二叉树:先创建二叉树,然后再将二叉树转为平衡二叉树
二叉树转为平衡二叉树:变换的核心在于某一侧子树不变
解决因为新插入结点导致平衡破坏的问题:只需要修改最小不平衡子树即可(新插入结点往上找第一个平衡被破坏的子树(结点))
总的结果是以新结点的父结点为根结点,
RR右单旋转法:一颗原本是平衡的二叉树在c的左子树(b)的左子树(a)插入结点导致不平衡(c的左子树高度+1导致不平衡)c及c的右子树取代b的右子树,b的右子树变成c的左子树
LL左单旋转法:一颗原本是平衡的二叉树因为在c的右子树(b)的右子树(a)插入结点导致不平衡(c的右子树高度+1导致不平衡)c及c的左子树取代b的左子树,b的左子树变成c的右子树
LR先左后右:原本是平衡的二叉树因为在c的左子树(b)的右子树(a)插入结点导致不平衡(c的左子树高度+1导致不平衡)则c及c的右子树取代a的右子树,并令a的右子树为c的左子树。b及b的左子树成为a的左子树,a的左子树成为b的右子树
RL先右后左:原本是平衡的二叉树因为在c的右子树(b)的左子树(a)插入结点导致不平衡(c的右子树高度+1导致不平衡)则c及c的左子树取代a的左子树,并令a的左子树为c的右子树。b及b的右子树成为a的右子树,a的右子树成为b的左子树
性质:一个h层的平衡二叉树最少需要的结点数 n(h)=1+n(h-1)+n(h-2) n(h-1)为第h-1层需要的结点数 n(0)=0 , n(1)=1 , 且这棵树的所有非叶子结点的平衡因子为1
哈夫曼树:在含有n个带权叶结点的二叉树中,带权路径长度最小的二叉树成为哈夫曼树(最优二叉树)
构造哈夫曼树:给定n个包含权值的结点,将这n个结点作为一个含有n颗树的森林f,每棵树只含有一个结点,1.生成一个新的结点(它的权值为左右子结点的权值和),从f中取出权值最小的两颗树作为新结点的左右子结点(无左右顺序)(所以哈夫曼树是不唯一的)2.从f中删除取出的两个结点,然后将新结点放回f中,3.重复至f中只有一颗树为止
性质:1.每个初始结点最终都是叶子结点
2.构造过程创建了n-1个新结点,所以哈夫曼树的总结点数为2n-1
一颗度为m的哈夫曼树只有度为0和度为m的结点1
3.哈夫曼树中不存在度为1的结点
哈夫曼树的应用:用哈夫曼树生成可变长度编码(哈夫曼编码)
可变长度编码:即对于一个字符串序列,我们可以用二进制表示,每一个特定的字符都由特定的二进制表示,所有字符都由不同位的二进制数表示,
固定长度编码指每一个特定的字符都由固定的长度的二进制表示,比如所有字符都由三位二进制数表示
前缀编码:没有一个字符是另一个字符的前缀(比如a为001,则不能出现一个字符为0或00)
假如在一串字符序列中a出现3次,b出现4次,c出现2次,则我们可以认为出现的次数为字符的权值,则构建一个森林有abc三颗树,每棵树都只有一个结点,然后用这个森林构建哈夫曼树
然后我们将构建好的哈夫曼树每个结点左的边设为0,右的边设为1,则叶子结点的编码为从上而下对边上的0/1排序
注意:最大编码长度为哈夫曼树的深度-1

不能为空(图中的结点间不一定要有连线,但不能有线而没有结点)
图由顶点集(结点的总数)和边集组成,(v,e)表示一个图,v为顶点集e为边集,用|V|表示顶点的总数
无向连通图中所有顶点的度之和为偶数
在题干没有特别说明的情况下,不考虑有从结点到结点自身的边。其值默认为0

图的相关概念

有向图,无向图:指边是有向,无向的图。无向图,最多有n(n-1)/2条边。有向图最多有n(n-1)条边
简单图:1.不存在重复边 2.不存在顶点到自身的边,则为简单图
多重图:与简单图相反,即图中存在(a,b),(b,a),(a,a),数据结构不讨论多重图
完全图:任意两个结点间都存在边。刚好有n(n-1)/2条边的是无向完全图,有n(n-1)条边的是有向完全图(即任意两个结点间都存在方向相反的两条边(即任意(a,b)在图中都存在(b,a))
子图:若a图为b的子图,则b图包含a图的所有边,a可以等于b ,若a和b有一样的顶点,则称a为b的生成子图
连通:任意
两结点间有路径存在(单一个结点也是连通图) 强连通:指a与b互相连通,即可从a到b也可从b到a,连通图指图中任意两个顶点都连通的,强连通图指图中任意两个顶点都是强连通的
连通分量:一个无向图g的连通子图g1,若在g中找不到一个包含g1且不同于g1的连通子图,则称g1为g的连通分量(即g1为最大的连通子图)
强连通分量则为有向图中的最大的连通子图
生成树:包含全部顶点的一个极小连通子图(边最少的为极小连通子图)
生成森林:非连通图的所有连通分量的生成树组成生成森林
广度优先遍历会得到一个高度最小的生成树,深度优先遍历会得到高度最大的生成树
顶点的度:一顶点与其他顶点连线的条数,无向图中,全部顶点的度的和=边数*2
出度和出度:在有向图中,以某结点为起点的边的条数(即结点a指向其他结点的边的条数)称为出度。入度是指在有向图中,以某结点为终点的边的条数
:带权的图称为网,也称为带权图
回路/环指从a到a的路径,例:a-b-a。一个有n个顶点的图,边数>n-1,则此图一定有环
简单路径:顶点不重复出现
简单回路:除第一个顶点和最后一个顶点外,其余顶点不重复出现
有向树:一个顶点的入度为0,其余顶点的入度都为1的有向图称为有向树
:有向图中的边

图的存储

1.邻接矩阵法:对于稀疏图会浪费许多空间
实现:定义一个一维数组存储所有结点。然后根据图生成一个邻接矩阵(二维数组),即从一维数组的第一个元素(结点)开始,到其他所有结点,有边则为1,无边则为0。
特点:1.对于无向图 ,矩阵为对称矩阵(左上,右下斜线对称),
且第i行/列的非0元素的个数为顶点i的度数
2.对于有向图,第i行非无穷元素(非0元素)的个数为顶点i的出度,第i列则为入度
3.对于带权图来说,有边的存放权值,没有边的为正无穷
4.邻接矩阵A^2中第i行j列的元素表示从i到j的路径的条数
A^n中第i行j列的非0元素表示从i到j的长度为n的路径的条数
所谓以行主序表示为在矩阵中第i行第j列的数表示为(i,j),以列主序则为(j,i)
2.邻接表法:适用于稀疏图,同一个图建立的表是不唯一的
用一个数组存储每个结点(每个结点的头指针)
为每个结点创建一个链表(单链表)用于存储从这个结点出发的边
边结点存储弧头顶点的下标和指向下一个边的边结点指针
3.十字链表:专用于存储有向图的链式存储结构
定义一个数组,数组每个元素存储一个结构,结构中定义三个变量,一个是数据,一个是出边的头指针(从结点出发的边),一个是入边的头指针
(出边的头指针存储从结点出发的所有的边,然后其他的变量的作用都是用于区分的查找这些边是从这个结点出发还是以这个结点为终点)
对于每个头指针定义一个单链表,链表中每个结点存储5个变量(其中一个存储权值),分别是边尾的结点和边头的结点(数组中的下标,边尾指从a指向b的边中的a)(int)
头边和尾边,头边指指向终点相同的下一条边 尾边是指指向起点相同的下一条边(即表示两条边都有共同的终点或起点) ,(当一个结点有三条边以他为终点时应该如何指?)
单链表中的每一个结点都是一条边(称为弧结点)
4.邻接多重表:专用于存储无向图的链式存储结构
定义一个数组,数组中每个元素存储两个变量,一个是数据,一个是该结点的边的头指针(任意一条?)
定义一个单链表,链表存储四个变量(还可以加标记(该边是否被访问)和该边的权值)四个变量分别是这条边的两个结点在数组中的下标(int)(a,b),和指向连接a的下一边的指针和指向连接b的下一边的指针
在这里插入图片描述

图的遍历:

广度优先遍历:BFS和树的层次遍历差不多,只是多了一个变量用于标记该结点是否已经被遍历过
从第一个顶点a开始,依次访问其各个邻接的未被访问的顶点a1,a2,a3,然后对a1,a2,a3,从a1开始依次访问其邻接的未被访问的顶点,若最后依然没有访问所有结点则任选一个未被访问的结点重新开始循环
(将图画成树的形式,然后进行层次遍历即可)(算法和层次遍历比多个判断该结点是否已被访问过)
广度优先遍历的应用:求无权图单源最短路径:从该结点出发,到达其他所有结点的最短路径(边数最少)
广度优先遍历1会生成一颗树或者森林,由于图的邻接表存储不唯一,所以其生成树不唯一
深度优先遍历:DFS与树的先序遍历差不多,只是多了一个变量用于标记该结点是否已经被遍历过
实现机制:从第一个顶点开始访问,然后访问与他邻接的未被访问过的顶点b,然后再访问与b邻接的未被访问过的顶点,直到没有未被访问过的邻接的顶点,则依次退回上一次被访问的顶点,访问他们邻接的顶点,直到访问玩图中所有顶点
(概括来就是沿着一条路径一直访问沿途的顶点,直到终点则后退一个顶点继续走另一条路…)
邻接矩阵法遍历的顺序唯一,邻接表不唯一
深度优先遍历会生成一颗树或者森林,邻接矩阵法的生成树唯一,邻接不唯一
对无向连通图做一次深度优先搜索可以访问到图的所有顶点
两种遍历对于邻接表存储的图,其时间复杂度都为边+顶点数,空间复杂度都顶点数
对于邻接矩阵则时间复杂度都为顶点数的平方,空间复杂度都顶点数
对于有向图,要按方向遍历
深度优先遍历生成的树高于广度优先,深度优先总往最深的遍历,广度反之

图的应用:

最小生成树

带权无向连通图包含全部顶点的一个最小连通子图(且权值和最小)
一个图的最小生成树不一定唯一(所有边的权值都不同则唯一或图的边数为结点数-1)
创建最小生成树一:prim算法 时间复杂度v^2 适用于边密集的图
1.创建一颗空树,
2.将图中任意一结点(a)加入树中,
3.从图中找一条包含结点a且权值最小的边(ae),将结点e加入树中并将边ae加入树中
4.再从图中找一个和树中已有结点邻接的,且(边值)权值最小的结点,将结点及其边加入树中
然后重复4.直到树中包含图中的所有结点
创建最小生成树二:kruskal算法,借助并查集,适用于稀疏图,时间复杂度eloge
1.创建一颗树,包含图中所有结点,但不包含边
2.将图中的边按权值从小到大依次加入树中,如果构成回路,则那条边舍弃。直到树中包含所有结点(树与图的区别在于图会构成回路,树不会,即有三个结点互相连通)
这两种算法构造的最小生成树可能相同,可能不同

找最短路径

带权有向图中一个结点到另一个结点所经过的边的权值和最小。最短路径一定是简单路径(出自严蔚敏)
找最短路径的算法无法判断一个图是否有环(最短路径算法允许有环图)
单源最短路径(Dijkstra算法):最终会得到从第一个结点到其他所有结点的最短路径,包括序列
不适合有权值为负数的图,时间复杂度为V^2,(也适合求任意两个顶点间的路径)
求依次得到的各最短路径的目标结点,这个顺序=各顶点按路径长度排序
思想:dist[]用于存储到每个点的最短路径,初始时dist等于下图m的第一行
从m的第一行一直遍历到最后一行
对于第二行,第二行存在2个值表示通过结点2可以到达2个点,则计算到这三个点的路径,若小于dist存储的对应的值,则更新dist
对于通过2到3他的路线为1-2-3,时间为dist[2]+9=10<12,则更新dist[3]=10;对于通过2到4时间为dist[2]+3=4<无穷,则更新dist[4]为4.。因为dist存储是到达该点的最短路径,所以通过2到达任意节点的花费为dist[2]+2到该结点的花费
在这里插入图片描述
一.创建四个数组,dist[](表示第一个结点到其他所有结点的最短路径),s[](标识图中是否还有结点未被计算),path[](表示每个最短路径的序列,如ae的序列可能为abe或ae),
m[][]用于存储图,有边的存储权值,无边的设置为无穷大
他们的大小为图中结点的数量,以下标表示图中的结点
二.初始化:dist[]存储上图的第一行,
s[]表示该结点是否已经被计算,1为被计算,0为未被计算,初始化将第一个结点设置1
path[]表示第一个结点能到达哪些结点,能到达的则将该下标下的值设置为该结点的下标(每个下标表示一个结点,如下标0表示第一个结点,将下标为三的值设置为0则表示0到3有一条边)
三.循环执行,直到s[]中所有元素的值为1为止
1.从dist[]中找出最小的值,这个值表示a到达的权值最小的边(ae),将将s[]中e的下标修改为1
2.将e这个结点作为计算的对象,若e可以到达b和c,分别比较ab,ae+eb(aeb)和ac,ae+ec(aec)的大小(无穷大为最大)若左边大,则用小的值代替大的值(ab>aeb,则令ab=aeb(修改dist[])),如果左边小则不做修改
3.若aeb最小,则将s[]中b的下标修改为1,将path[]中e的下标修改为a的下标,b的下标修改为e的下标,
继续从1开始循环
四.从path[]中获得最短路径的序列,因为path中存储的是以该结点为终点的结点的下标,所以我们只要从下标找
比如从第一个结点到第三个结点的最短路径,如果path中下标3存储的值为0,则序列为03
如果值为2,则找2的下标的值,如果2的下标为0则序列为023
实现方法二(Floyd算法):计算了所有结点间的最短路径,适合有权值为负数的图,但这个负权值的边不能在回路中。时间复杂度为V^3
1.创建一个矩阵A^-1和存储路径的矩阵path:大小为图中结点的总数(方形n*n),
A中每一列代表每一个结点,每一行存储这个结点到其他结点的权值(最小的权值),即i,j表示i到j在没有中转的情况下的最短路径(i直接到j)。
比如第2行第3列存储第二个结点到第三个结点的权值(有向,无边为无穷大,到自身为0),
path初始所有值为-1,path中第i,j存储的值表示从i到j的中转结点的下标(一次中转)
2.以第一个a结点为中介点,计算以a为中介点的最短路径(例:b-a-e),如果这个值小于原值(b-e),则替代原值(则be=bae)(更新A^-1为 A^0 ,A^0表示允许以a作为中转结点,则各个结点间的最短路径)
且在path中修改be的值为a的下标(即从b到e的中转结点为a)
3.选以a相邻的结点作为第二个结点中转结点,即b和a一起作为中介点,计算以a,b为中介点的最短路径(例:f-b-a-e),如果这个值小于原值,则替代原值(A^0 到 A^1)
4.继续以相邻的下一个结点为中介点,循环,直到所有结点可以都作为中转结点

//Floyd算法:准备工作,A和path
for (int k=0 ; k<n; k++){/考虑以 vk作为中转点
for(int i=0 ; i<n; i++) {//遍历整个矩阵,i为行号,j为列号
for (int j=0; j<n; j++){
if (A[i][j]>A[i] [k ]+A[k][j]){//以 vk为中转点的路径更短
A[i][j]=A[i][k ]+A[k][j];//更新最短路径长度
path[i][j]=k ;//中转点
}}}}

有向无环图描述表达式

DAG图:对用二叉树描述的表达式作优化,共享相同子式,节省空间(不可以改变左右顺序)
在这里插入图片描述
在这里插入图片描述

拓扑排序

对DAG图排序,使其结点间有自己想要的先后顺序(AOV网),结果不一定唯一
DAG图:有向无环图。拓扑排序是对DAG图进行排序
AOV网:有向无环图。用DAG图表示一个工程,每个结点表示一项活动,有向边表示活动发生的先后关系,则称这种DAG图为AOV网
拓扑排序是输出一个工程的一个执行顺序
若一个图不能有拓扑序列,则该图一定是有环的(一定是顶点数大于1的强连通分量)
拓扑排序存储入度为0的顶点在两个顶点没有前后关系时可以用栈存储
AOV网的性质:1.每个顶点出现且只出现一次
2.若存在一条从A到B的路径,则排序中B在A的后面(不存在从B到A的路径)
拓扑排序的实现:1.从DAG图中选择一条没有入边(前驱)(没有一条边的终点是这个结点)(=只有出边)的结点
2.输出这个结点并删除包含这个结点的所有边,
3.然后继续在图中选择一条没有入边的结点,重复12,直到图为空或者没有一条没有入边的结点
若邻接矩阵为三角矩阵则存在拓扑排序,否则不一定存在拓扑排序
该算法对于邻接表的时间复杂度为V+E,邻接矩阵为V^2
逆拓扑排序:1.从DAG图中选择一条没有出边(后继)(没有一条边的终点是这个结点)的结点
2.输出这个结点并删除包含这个结点的所有边,
3.然后继续在图中选择一条没有出边的结点,重复12,直到图为空或者没有一条没有出边的结点

关键路径

从源点到汇点权值和最大的路径称为关键路径,关键路径上执行的活动叫关键活动
(这个关键路径包含所有顶点),每个边表示该事件完成所需的时间。非关键路径的事件是可以拖延的,而关键路径上的事件不能拖延
AOE网:和AOV网类似,在有向带权图中,每一个结点代表一个事件,有向边代表事件执行的的顺序(活动),边上的权值代表完成该过程的开销,我们称这种网为AOE网(比AOV网多一个权值)
特性:1.有且仅有一个结点叫做源点(从这个事件开始),他只有出边 有且仅有一个结点叫做汇点(以这个事件结束),他只有入边
2.一个顶点a,只有当a事件结束后,从a出发的事件(a的出边)才能开始。只有当a的入边(以a为终点)的事件都结束后,a事件才能开始
3.要缩短工期必须缩短所有关键路径共同发生的活动的时间才能缩短工期,而延长只需延长任意一条关键路径中任意一个活动
找出关键路径的步骤
1.求最早发生时间:利用拓扑排序获得一个序列
依次求出这些事件的最早发生时间:初始时间+选所有路径中权值和最大的路径,其和
2.求最迟发生时间:利用逆拓扑排序获得一个序列
依次求出这些事件的最迟发生时间:而第一个顶点为汇点,则其最迟发生时间等于最早发生时间
从第二个顶点开始,最早发生时间=该顶点后一个顶点的最早发生时间-他们之间边的权值
若一个顶点有多个出边,则比较其各个出边结点的最早发生时间-他们之间的边的权值,取最小值即可
3.求活动最早发生时间,活动最早发生时间等于这条边的出发点(事件)的最早发生时间
活动最迟发生时间等于这条边的终点(事件)的最迟发生时间-这条边的权值
4.活动的差额:活动最迟发生时间-活动最早发生时间,等于0则为关键活动(关键路径)
当关键路径不唯一时,选包含结点最多的关键路径即可

查找算法

概念:查找表:被查找的数据集合(从查找表中查找(匹配)内容)
查找长度:在查找到一个元素前需要比较多少个数据才能查找到这个元素,
平均查找长度:这个查找表中n个元素的查找长度之和/n
也=每个元素的 查找概率*找到该元素所需要的比较次数的和(这个比较次数只和该元素在表中的位置有关,在第一个位置即比较一次,在第6个位置即比较六次)
顺序查找:,也称线性查找。即一个一个的比较。对有序表和无序表的成功查找时间相同
折半查找:又称二分查找,仅适用于有序的顺序表(链表不适用),时间复杂度为log2^n
首先将关键字与中间元素比较,若中间元素等于关键字,则返回查找成功,若大于关键字,在升序序列中则将关键字与查找表前半部分的中间的值继续比较
判定树:判定查找失败的树,该树是一颗平衡二叉树
将表中的元素按折半查找的比较顺序构成一颗二叉树,(左结点小于右结点)。所有叶结点存储该结点的范围(即左结点小于父结点,右结点大于父结点的范围)。比较到结点则说明比较失败。
N=255,查找不超过最多比较8次(折半查找,书上的)

void HalfSearch(E A[],E x){	//从一组升序序列中找到给定值x,E表示任意类型
int low=0,high=n-1,mid;	//high和low分别指查找序列的的上届和下界的下标,n是顺序表的长度,mid是中值下标
while(low<=high){	//low会一直小于等于high,直到找到x,若low大于high则序列中不存在给定值x
mid=(low+high)/2;
if(A[mid]==x) break;	//找到x,结束循环
else if(A[mid]<x)  low=mid+1;//如果中值小于x则说明x在中值右端,则以右端为新的序列开始查找
else high=mid+1;		//如果中值大于x则说明x在中值左端,则以左端为新的序列开始查找
}

分块查找:结合顺序查找和折半查找
即将查找表有序的分为若干块,块内元素是无序的,而各个块间是有序的
如:24.21.32.54.72.61.78.88.83这一个表可以按最大值为24.54.78.88.分为4个块
24.21-- 32.54-- 72.61.78-- 88.83。分为这四个块
将这四个值及这四个值所在块建立一个索引表,例24为1开始54的块为3开始78以5开始
若查找值为30,则于索引表比较知其在54的块内,所以只要在表第3到第5中比较即可
完全索引表,等长索引表

B-树

多路平衡查找树,(所有结点的平衡因子=0),树中结点的最大孩子结点数为该树的阶数
B树每个结点的孩子结点中关键字的值的存储范围是根据父结点中关键字的大小来决定的
例:一个结点包含关键字20,25,35,则这个结点有4个孩子结点,每个孩子结点中关键字的存储范围分别为0~20 , 20 ~ 25,25 ~ 35,35 ~ 正无穷。结点中关键字是按升序排序列的(亦可以是降序)
特征:1.n阶B树每个结点最多存储n-1个关键字,因为在B树中一个结点的孩子结点的个数=关键字的数量+1。树中每个结点最多有n个孩子结点(包括根结点) B树中描述的所有的n/2都是取上界的
2.除根结点外所有非叶结点至少有n/2个孩子结点,即至少有n/2-1个关键字
3.树的高度h的取值范围 [logn/2^ [(m+1)/2] ] +1>=h>=logn^(m+1) m是树中总的关键字的个数
4.B树不支持顺序查找
插入:当插入后结点的关键字个数大于n-1时,必须对结点进行分裂
将改结点中的关键字从中间位置分裂成两个结点,中间的关键字加入父结点的关键字中
删除:要使删除后结点的关键字数>n/2-1
1.当被删除的关键字所在的结点不是叶子结点,则用该关键字的前驱或者后继关键字替代该关键字(前驱或者后继是在子结点中大于或者小于该结点的相邻的关键字)
2.当被删除的关键字所在的结点是叶子结点时,再分两种情况,若被删除的关键字所在的结点的左右兄弟结点中关键字的个数>n/2-1(即兄弟够借),则从兄弟结点中取相邻的一个结点替换父结点中一个关键字,再将父结点中被替换的关键字替换被删除的关键字
若兄弟不够借则删除该结点,再从父结点中移一个结点下来和不够借的那个兄弟结点合并为一个结点
在这里插入图片描述
在这里插入图片描述

-B+树:适应数据库的需要而诞生
B+树和B-树的不同在于结点的孩子结点数少1,(少那个35 ~ 正无穷)
叶节点包含; 树中的所有结点,且叶节点是按顺序从左到右分布的,都通过指针连接在一起

散列函数

将查找表中的关键字通过散列函数映射在散列表中
散列表:即给一个查找表,将查找表中的关键字用散列函数计算出其地址(下标)然后按结果存储在顺序结构(数组)中,这个顺序结构叫散列表
散列函数:通过函数计算得到每个查找表中的关键字在散列表中的位置,这个函数叫散列函数
同义词:散列函数指把两个不同的关键字映射在同一个地址上(散列表中同一个下标)
例:对于查找表{1,5,6},给定一个长度为3的散列表。定义散列函数y=x%3,则查找表中的数据通过散列函数映射在散列表中为{6,1,5}
散列函数特点:1.x的定义域要能包括查找表中所有的值 2.值域y应该小于等于散列表的长度
3.散列函数应该减少冲突的发生 4.散列函数的返回值不能是随机的,因为要用于查找
几种常见的散列函数:1.直接定址法:散列函数为ax+b。特点:简单,不会产生冲突,适合关键字发布连续的情况。若不然会造成空间浪费
2.数字分析法:
3.平方取中法:取关键字的平均值的中间几位。优点:散列地址分布较均匀
解决冲突的方法:1.开放定址法:不能随便删除某一元素
2.线性探查法:如果出现冲突,向下查找一个空位存储即可。查找时也是,如果按照散列函数找到地址,但这个地址存储的却不是目标数,则向下查找即可,若找到空处则说明查找失败
3.链接法:将所有同义词存储在一个线性链表中。(创建一个数组,每个下标是一个单链表,下标值是散列表中下标)查找时,若在对应地址没有找到目标元素,则查找对应地址对应单链表
4.再散列法:即再通过一个函数以确定发生冲突的关键字存储的地址
例:H(0)=x%13 H(i)=(H(i -1)+(x+1)%11)%13;则对于关键字27,H(0)=1,H(1)=7,若地址7已经有关键字,则又发生了冲突,则H(2)=0
装填因子:a=散列表中的数据数/散列表的长度 。a的值越大,说明发生冲突的概率越大
若散列函数=x%y,则y应该取不大于散列表的长度的最大的素数(指像7,11,13这种不能被整除的数)
探测指一次没有查找成功后序进行的查找
利用线性探查法时,定位到散列表中的位置,,查找失败所需要的比较次数要比查找成功的次数多1
即若散列表长度为5,散列表为{6,1,5},定位到下标为1位置,若查找失败则需要比较3次
哈希冲突函数是用于解决冲突的函数,再散列。哈希函数中某些方法书上的与408不同
处理冲突时尽量减少产生聚集/堆积现象

排序算法

内部排序:数组小于或等于内存时,在内存中进行的排序
外部排序:数组大于内存时的排序(只有多路归并排序)
稳定性指在排序前关键字a在b前,排序后a依然在b前,则是稳定的
算法的性能取决于空间复杂度和时间复杂度(比较次数和移动的次数。排序的比较次数至少为log2^(n!))
主关键字指每个元素都不同的能唯一区分各个元素的关键词,而不能做到唯一区分的就是次关键字

内部排序

直接插入排序:直接插入排序最好情况下时间复杂度为n。最坏为和平均是n方
最坏情况下的比较次数为?
每一个将被排序的序列都可以被分为三个部分,前一部分是有序序列,第二部分是待插入的值,第三部分是无序序列(待排序的值的序列)
例49,38,65,98,12.若递增排序,初始时,49单独构成有序序列,38是待插入值,其他为无序序列。第一次操作将38插入到49前,则新序列的有序序列为38,49,65,98.则12为待插值,无无序序列
1.将被排序序列存储在数组中(sort[]),下标为0的空间空出来
2,定义一个值记录待插入的值的下标(i),定义第二个变量记录有序序列中要与待插入的值比较的下标(j),这个值初始为有序序列中最后一个值(j=i-1)
3.假如是升序排序,将待插入的值复制到A[0]位置,然后将待插入的值(A[0])与有序序列最后一个值比较,如果i大,则不变,如果i小,则让i与j互换,然后j-1

void sort(A[],int n){//升序排序
int i , j; //i记录待插入的值的下标,当最后一个值已经插入时,结束整个循环,j记录有序序列中要与待插入的值比较的下标,这个值初始为有序序列中最后一个值
for(i=2 ;i<=n;i++){
A[O]=A[i]; //将待插入的值复制到A[0]位置,利用A[0]作比较
for(j=i-1;A[j] .key>A[O] .key;j--){//当有序序列最后一个值大于待插入的值时执行循环
A[j+1]=A[j]; //使后一个值等于前一个值
}  //每次j--;将待插入的值与有序序列中每一个元素进行比较,大的后移(移出来的空放待插入的值),直到遇到小的或循环到与A[0]比较,
A[j+1]=A[0]; //当j=0时,上一个循环退出了,没有执行A[j+1]=A[j];语句,这里补上(A[1]=A[0])
}  }

折半插入排序:折半查找和直接插入排序的结合,时间复杂度为n^2 。比较次数为n log2^n
即先通过折半查找在有序序列中找到待插入元素该插入的位置
希尔排序:不稳定,最坏时间复杂度为n^2。适用于基本有序,数据量不大的情况
例对于6个数 1,3,8,5,2,4.先算出增量,一般取n/2取下界
取增量为3,则将第i个元素与第i+3个元素组成一组,知道i+3>6
1,5。。3,2.。。8,4这三组,每组进行直接插入排序
按升序结果是1,2,4,5,3,8.然后对3/2取上界(增量)继续分组,
1,4,3。。2,5,8.两组,每组进行直接插入排序。然后继续取增量直到增量为1

void shellsort(ElemType A[] , int n){
for(int dk=n/2; dk>=1; dk=dk/2){ //每次分为2/dk取下界个小组
for(inf i=dk+1; i<=n; ++i){  //下面所有代码执行一次分组后,对所有组进行直接插入排序(这个直接插入排序是所有组同时进行的,先i+1,然后判断i<n)
if(A[i].key<A[i-dk] .key) {  //下面代码执行一组内的一次直接插入排序
A[O]=A[i];
for(int j=i-dk; j>0&&A[O].key<A[j] .key; j-=dk){//每次循环ij为不同小组内的的两个元素,并进行比较和互换
A[j+dk]=A[j];}
A[j+dk]=A[0];
}}}}

冒泡排序:稳定的,n方
对待排序序列从第一个元素开始,两两比较,每次比较将较大的值放后面,直到比较到最后一个元素,这样会将序列中最大的值移到尾部(一次冒泡),不断循环这个过程直到有序就停止冒泡
快速排序:(不稳定),最坏时间复杂度为n^2 ,平均时间复杂度为 nlog2^n
最好平均空间复杂度为 log2^n 最坏空间复杂度为 n
快速排序是所有内部排序算法中平均性能最优的算法
选一个值作为基准值(枢纽),(默认找第一个值),一次快速排序会将一个值放在这个序列排序好后应该放的位置(这个值前部分的数都小于这个值,后部分都大于这个值)
1.拷贝第一个数(即将第一个数的位置空出来),然后从右向左(定义一个标记变量end)找比第一个数小的值放在第一个数的位置(则右边空出一个位置),
2.然后从左向右(定义一个标记变量top)找比第一个数大的数放在右边(放在空位)
3.当top==end时结束一次排序。将基准值放入top位置。
4.然后对前后两个分区的数重复123步
快速排序适用于数据不大的情况,因为适用递归会占用空间
当待排序序列基本有序时,快速排序速度最慢,当每次枢纽都能将序列等分时,速度最快(枢纽是中值)
快速排序可通过三者取中的方法来避免最坏情况发生
选择排序:(不稳定)选择排序和冒泡排序相似,选择排序每次从序列中找取最小的数放在首部(交换)(每次通过一个一个数的比较获得序列中的最小值,第一次比较n-1次,第二次比较n-2次…)
比较次数为n(n-1)/2 (时间复杂度为n^2)。最坏移动次数为3(n-1)
堆排序:(不稳定)h(高度=log2^n) 。堆排序时间复杂度为nlog2^n 。
堆排序中关键字的比较次数为调整完毕的次数+1(这一次用于判断已经调整完毕)
分为两个部分,创建堆和输出堆顶元素调整堆的过程
每次选择一个最小值需要比较n-1次。若把待排序序列构成一个完全二叉树再获得最小值可以减少比较次数。构成完全二叉树(建堆)需要时间为n,从二叉树获得最小值(调整)需要比较log2n次,所以时间复杂度为nlog2n
小根堆:第i个元素小于第2i和第2i+1个元素的序列,大根堆反之,(1<=i<=n/2)
大根堆:树中根结点大于其左右孩子结点,左右孩子结点无相对大小关系
排序过程中将小大根堆看作一颗完全二叉树
堆顶元素就是序列中的最大(最小)值,输出这个值后会破坏堆的性质,则需要调整堆
一.将一个待排序序列初始化为大根堆:
1.先将所有元素形成一颗二叉树中,(第一个元素为根,第二三个元素为其左右子结点,对于左子结点,第4第5元素为其子结点,则第6第7元素为第3元素的子结点。。。)
2.然后从序列最后一个元素开始调整。若子结点最大值大于父结点,则将这个值与父结点互换
从下到上调整所有结点,若依然未形成规则(大根堆子结点小于父结点),则继续从下到上调整
二:输出根结点后的调整:将最后一个结点作为新的根结点,然后从上往下开始调整
插入:插入到树的尾端,然后从下向上调整即可
小根堆中,关键字最大的数可能存储在n/2+2的位置{([n/2]+1)取上界~n}

//Java代码,c语言的实现类似,改一下函数及变量的创建方法即可
public static void sort(int[] arr,int s,int e) {//s和e是指对数组的第s个元素到第e个元素排序
        //int n = e-s+1;
        // 构建堆
        for (int i = (e+s-1) / 2 - 1; i >= s-1; i--) {heapify(arr,s, e, i);}//最右边最下面的一个父结点在下标n / 2 - 1位置,因为一颗二叉树的前面n/2个结点都是从上到下从左到右排列的父结点
        // 堆排序
        for (int i = e - 1; i >= s-1; i--) {
            // 将堆顶元素与末尾元素交换(出堆)
            int temp = arr[s-1];arr[s-1] = arr[i];arr[i] = temp;
            // 对剩余元素重新构建堆,每一次构建堆会将最大值移到根部,根是数组的第一个位置
            heapify(arr, s,i, s-1);}}
    // 堆化操作
    private static void heapify(int[] arr, int s,int n, int i) {
        int largest = i;// 下标为i的为根节点
        int l = 2 * i + 1-s+1;// 左孩子节点
        int r = 2 *  i +2-s+1;// 右孩子节点
        // 找出三个节点中的最大值
        if (l < n && arr[l] > arr[largest]) {largest = l;}//将这一行和下一行的小于改为大于即为降序排序
        if (r < n && arr[r] > arr[largest]) {largest = r;}
        // 如果最大值不是根节点,则交换根节点和最大值,并继续向下堆化
        if (largest != i) {int temp = arr[i];arr[i] = arr[largest];arr[largest] = temp;heapify(arr, s,n, largest);}}
}

归并排序:(两路归并)稳定 时间复杂度为nlog2n 空间复杂度为n(实现)(占空间最多的排序算法)
将一个待排序的序列(总长度为n)非为n个块,然后将每两个元素组成一个有序的块,然后继续使每两个块合为一个有序的块,直到只剩一个块
2路归并排序,指每次使两个块归并
将两个块合并的算法:创建一个数组存储两个块的数据,从两个块中第i ,j个数开始比较,若i 小,则将i
加入到结果序列中,并i++,若j小,则将j 加入结果序列中,并j++。直到一个块中的所有元素都加入了结果序列,则将另一个块中剩余的数全加入结果序列即可

基数排序:稳定,时间复杂度为O(r(n+q)) 空间复杂度为q
对于一组待排序序列,有n个值,序列中最大的值的长度为r(r=3,则最大值为百位数),每一个数的取值范围为q(q=10,则取值范围可能为0~9)
排序过程一共循环r次,需要q个辅助队列,下面是最低位优先算法(LSD)(即先从最低位排序)
高位优先为(MSD)
1.定义q个队列
2.将每一个待排序的值按个位数的大小放入对应队列中,即个位数为0的放入第0个队列中,
3.从第0个队列开始依次从队列中取值,组成一个新的序列(新序列按个位数的大小排序)
4.将每一个待排序的值按十位数的大小放入对应队列中。。。(重复23)
排序算法的选择若n较小时可选直接插入排序或简单选择排序
若已基本有序选直接插入排序或冒泡排序为宜
若n较大时应采用时间复杂度在nlog2^n的算法。当关键字随机分布时快速排序平均时间最短
选择排序的比较次数和待排序序列的初始状态无关(初始关键字的顺序)
交换排序的趟数和初始状态无关

外部排序

总时间=内部排序时间+外存信息读写时间+内部归并时间
在待排序序列的大小大于内部存储空间的大小时,应该使用外部排序,外部排序基于归并排序实现
因为外部磁盘的读写速度是远慢于内存的,所以与外存的i/o次数是主要影响因素
严格k叉树:只有度为k与度为0的结点的k叉树
外部排序一
1.假设内部空间大小为n,我们将待排序序列分为a个块,每一个块的大小为n,将每一个块用内部排序算法排序成有序序列,这些有序子文件称为归并段或顺串
2.将内部空间分为三个部分,前两个空间用于存放两个块中的前几个元素,后面一个块存放输出结果
3.比较前两个块中最小的值放入输出空间中,当输出空间满的时候输出结果,当前两个块中某个块中的所有元素都已存在与结果中时释放该空间,并将该块剩余的元素添加到块中继续比较
4.重复循环
外部排序二:败者树使内部归并不受归并路数k的增大的影响,每次输出一个最小(递减)或最大值
1.假设内部空间大小为n,将待排序序列分为a个块,每一个块的大小为n,将每一个块排序成有序序列
2.定义一个完全二叉树,使所有叶子结点存储待比较的值,每一个叶子结点分别存储一个块中的第一个值(即待比较的值)(有a个叶子结点)
3.叶子结点的父结点存储它的两个子结点中最大值的块的编号(即每个块都有编号)(在递增排序中,较小的值为胜利者,较大的为失败者)
每个结点存储左右子树中的失败者,而胜利者则会继续上传继续比较。(即每个结点会代表胜利者继续和它的兄弟结点做比较,将胜利者上传并记录失败者)
3.到根节点后会输出该树中最小的值,并传入最小的值所在的块的第二个值继续比较
k个记录(关键字)中选择最小关键字最多需要log2^k(取上界)次比较
所以总的比较次数为(n-1)log2^r(取上界) ,r为归并段的数量

外部排序三:置换-选择排序产生更长的初始归并段以减少归并趟数
设有待排序序列a,工作区b(内存,大小为3),输出文件c
1.从a中拿出三个值放入b中(从头开始拿),
2.从b中取最小值(q)放入c中
3.然后从a中拿一个值放入b中(保持b中有三个值),取b中最小且比q大的值放入c中
4.循环2.3,直到b中所有值都小于c中的值,则输出c
5.继续循环2.3.4.直到a为空,这样的结果就是生成了n个有序的长度不等的块

外部排序三:最佳归并树对置换-选择排序所生成的归并段,组织归并顺序使得i /o次数最少
将哈夫曼树的思想推广到m叉树的情形
将每个归并段中关键字的个数看作一个结点构造生成哈夫曼树,严格k叉树
缺少的结点用0补 补充的0的结点称为虚段
1.先计算出需要补充的虚段的数量,已知归并段数为a,m叉树…则虚段数=(m-1)-(a-1)%(m-1)
2.先将虚段添加到结点森林中,然后开始构造哈夫曼树

一个缓冲区对应一个输入/输出文件
a路平衡归并排序中,并行处理需要2m个输入缓冲区和2个输出缓冲区,
进行串行操作需要m个输入缓冲区,1个输出缓冲区

经典例题

算法分析
第一步,找可以作为循环条件结束的点(如果是找值问题,则思考如何能取到这个值,)
第二步:思考特殊情况下自己的算法是否依然成立(比如取临界值时,比如存在0或负数,比如比较中的相等的情况,还有其他情况视题目而定,比如找中值问题中的当一个数组中所有数都小于另一个数组时,比如取数组中的数的个数分为奇偶讨论)

算法

要求在序列中处理重复数据的问题且对空间复杂度无要求的都可以用类似下面这个算法解决
1.在任给的一个全是整数的序列中,找出最小的,未出现的正整数,已知数组长度为n
思路:若序列为{5,-1,-3,1,3},则最小的正整数应该为2,因为没有比1更小的正整数
考虑创建一个新的数组以解决问题,设置新数组的初始值为0,从给定序列的第一个数开始扫描,将扫描的正的数放入新建数组对应的下标中,直到扫描到序列的结尾,然后扫描一遍新建数组,则第一个值为0的下标就是所求的未出现的正整数
2定义D=|a-b|+|b-c|+|c-a|,a,b,c分别取自三个升序数组A,B,C,求D的最小的值

//思路:D=|a-b|+|b-c|+|c-a|其实等于2L,L是三个数中最大值和最小值的差,
所以只要不断的从abc中找到最小的数并使其下标+1(使其增大),然后找到一个最小的D,直到数组末尾
#define MAX 0x7fffffff	// 0x7fffffff为int类型的极限值
int enPulse(int a){	//用于计算绝对值
if(a<0){return -a;}else{return a;}}
bool min(int a,int b,int c){	//用于获得a,b,c中的最小的数
if(a<=b&&a<=c){return ture;}else{return false;}}
int Dmax(int* A,int* B,int* C){
int i=0,j=0,k=0,Dm=MAX,D;
while(i<x&&j<y&&k<z&&Dm>0){	//xyz分别是ABC中数的个数
if(D<Dm) Dm=D;		//更新D
if(min(A[i],B[j],C[k])) i++;	//若a是最小值,则下标加一
else if(min(B[j],A[i],C[k])) j++;
else k++;}
return Dm}

1.线性表

1:一个带头结点的循环单链表,头指针为h,有h.next->next=h成立,则该链表的长度可能为?
思路:若链表长度为0,则h.next=h,则h.next->next=h.next=h,所以链表长度可能为0
若h.next指向第一个结点,则给该链表长度可能为1,所以该链表长度可能为0或1
2.找到单链表的中间结点的一个较优的思路
思路:设置pq两个指针初始值为头结点,p往下走一步,q走两步,当q结点为末尾结点时结束循环,则p就是中间结点

p=q=h;
while(q.next!=NULL){
p=q.next; 	q=q.next;
if(q.next!=NULL) q=q.next;//若q后面只有一步,则只走一步(处理奇数情况)
}

2.栈和队列

3.二叉树

0:解决一道算法题,应该先将描述的内容举例出来,根据简单例子观察规律
例:给出满二叉树的先序序列,求其后序序列
假如有一个先序序列ABCDEFG,则后序序列为CDBFGEA
很明显,先序序列中序列长度的一半(中间位置)为分割线,右边是右子树的先序序列,左边从下标1开始是左子树的先序序列,所以转后序只要将A放到末尾,BCD改成后序—CDB,EFG改成–FGE

1:设二叉树中各结点的值不同,将这颗二叉树的先序序列和后序序列分别存储在一维数组A和B中,设计算法建立该二叉树的二叉链表
思路:根据先序序列和中序序列的特征,将树分为多个子树来建立
因为在先序序列中第一个数为根结点,后面a个数为左子树的先序序列,再后面b个数为右子树的先序序列,a+b=n-1,而在左子数的先序序列中,也是第一个数为根结点,后面i个数为左子树的先序序列,再后面j个数为右子树的先序序列i+j=a-1… (且先序序列中所有的根结点是紧邻的)
根据中序序列则可以知道a,b,i,j的值,,中序序列也可以像先序序列一样划分为多个左右子树的中序序列(根的左边为左子树的中序序列,根的右边为右子树的中序序列)
总步骤:用递归,将一颗大树分为
1.A[i] (初始时i等于1)为根结点 2.在B中找到A[i]的位置
3.在B中获得以A[i]为根的树的左右子树的长度
4.开始递归,传入A[i]的左子树的序列在A,B中的位置(从下标x开始到下标y结束),并令i++(左根下标+1)
传入A[i]的右子树的序列在A,B中的位置(从下标x开始到下标y结束),并令右根下标+1

BiTree PreIncreat (ElemType A[],ElemTypeB[],Int ta,int ea,int tb,int eb) {//参数的初始值为ta=tb=1,ea=eb=n(下标1开始)
//ta.ea为先序的第一和最后个结点下标, tb.eb为中序的第一和最后一个结点下标
root= (BiTNode* )malloc (sizeof (BiTNode)) ;//创建根结点
root->data-A[ta] ;	//根结点
for(i=tb;B[i] !=root->data;i++) ;//在B中找到根结点
llen=i-tb;   rlen=eb-i;		//获得左右子树的长度
if (llen)		//递归建立左子树
root->lchild=PreInCreat(A,B,ta+1,ta+llen,tb, tb+llen-1);//传入左子树在A和B中的位置
else		//左子树为空
root-> lchild=NULL;
if (rlen)		//递归建立右子树
root->rchild=PreInCreat (A,B,ea-rlen+1,ea,eb-rlen+1,eb) ;传入右子树在A和B中的位置
else		//右子树为空
root-> rchild=NULL;
return root ;	//返回根结点指针

2

  • 1
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值