目录
第三章 栈和队列
3.1 栈
存储单元?
逻辑结构?
栈的基本操作?
C语言标识符?
上溢和下溢?
卡特兰数?
存储单元:一个存储单元可以存储一个字节(Byte=8bit),计算机的存储器容量是以字节为最小单位的,比如1KB的存储他有1024个存储单元,一个字母占一个字节,一个汉字占两个字节。一个字符等于两个字节。
逻辑结构:线性结构和非线性结构,指的是数据元素之间的逻辑关系,即从逻辑关系上描述数据,它与数据的存储无关,是独立于计算机的。
栈的基本操作:
InitStack(&S),StackEmpty(S),push,pop,GetTop(S,&x),ClearStack(&S)
C语言标识符:标识符的第一个字符必须是大小写英文字母或下划线
上溢和下溢:栈的插入和删除操作都是在栈顶进行的,只可能栈顶指针超出了最大范围,从而产生上溢,而不是下溢。
卡特兰数:对于n个不同元素进栈,出栈序列的个数为
3.2 队列
1.队列的顺序存储是怎么进行进队操作和出队操作的?
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1(注意先后顺序,题目中着重考察)
出队操作:对不空时,先取队头元素值,再将队头指针加1
注意:循环队列的出队操作和进队操作也是如此
2.用单链表实现队列时,队头在链表的什么位置?为啥子?
最理想的情况是一个循环单链表,带头结点,带尾指针。其中头结点头指针负责删除,尾指针负责增加,那么入队和出队的时间复杂度都为O(1),这时我们再来看队头指针就应该在链头了。
3.3 栈和队列的应用
1.页面替换算法是否用到队列?
是的,在FIFO,先进先出中用到队列
2.栈在表达式求值中,一般是用于存放操作符的。
3.执行函数的时候,局部变量一般采用什么结构进行存储?
栈结构,因为调用函数的时候,系统会为调用者构造一个由参数表和返回地址组成的活动记录,并将记录压入系统提供的栈中,若被调用函数有局部变量,也要压入栈中。
注意:在递归调用的过程中,系统为每一层的返回点,局部变量,传入实参等开辟了递归工作栈来进行数据存储。递归调用函数时,在系统栈中保存的函数信息需满足先进后出的特点。
3.4 特殊矩阵的压缩存储
1.三角矩阵A中的元素按行优先存放在一维数组B中,aij元素对应B中数组下标K应当是?
K=2(i-1)+j-1
若已知K,求i和j?
i=(k+1)/3+1,j=k-2i+3
2.什么是稀疏矩阵?拿来干嘛?
矩阵元素个数S远远大于非零个数t,S>>t的矩阵称为稀疏矩阵,用来存放非零元素,节省存储空间,稀疏矩阵压缩存储后便失去了随机存取特性。
3.什么是十字链表?(这部分引用了青岛大学的课程教学资料)
概念:十字链表是有向图的另一种链式存储结构。我们也可以把他理解为有向图的邻接表和逆邻接表的结合。有向图中的每一条弧对应十字链表的一个弧节点,每一个顶点对应一十字链表的一个顶点节点。
举个例子:
如这样的有向图,我们的十字链表应该怎么画勒
先来看看邻接表是怎么画的:
邻接表描述了每个点的出度节点,但是有个缺点是,要想知道一个点的入度节点的话,则需要我们遍历整个邻接表,所以我们引入了十字链表。
现在修改表头节点的指针域,分别是数据域,入度边,出度边
修改弧节点的指针域,分别是,弧尾,弧头(带箭头的那端指向的那个节点),弧头一样的下一条弧,弧尾一样的下一条弧
画十字链表的步骤是:先把所有的出度边画出来,然后再来画入度。
3.5 算法题遇到的一些问题
1.为什么在关于一些栈,队列,链表函数传递参数要用到&,而有些又不用?什么时候用?
取地址,我们就可以对这个变量进行操作,改变这个变量。不用&,就只能得到副本,可以看,但是不能动。一句话来说,当你要改变这个变量,就加取地址,不想改就不加。
2.数据结构里面的基本操作一般是可以直接用的,下面罗列一下:
(1)栈的基本操作: (2)队列的基本操作
InitStack(&S) InitQueue(&Q)
StackEmpty(S) QueueEmpty(Q)
Push(&S,x) EnQueue(&Q,x)
Pop(&S,&x) DeQueue(&Q,&x)
GetTop(S,&x) GetHead(Q,&x)
ClearStack(&S)
3.!是取非运算,都快忘了。比如stackEmpty(S)是判空,假设是true,那么!stackEmpty(S)之后就是false了。
4.指针的问题:
算法题不建议用指针,首先王道书上的算法答案本身就不一定是可运行的代码,保险起见能用数组就用数组吧。
char *str=“abc”和char str[]="abc"有什么区别嗷?
前面只能看,不能改。后面可以改。
5.算法题更注重的是逻辑清晰,而不是代码一定要可运行。一定要把备注写清楚。因为题目问也只是问个逻辑,而不是编程比赛。
第4章 树与二叉树
4.1 树的基本概念
1.树的定义是递归的,是一种递归的数据结构。树作为逻辑结构,同时也是一种分层结构。
2.树的路径长度怎么求?
是从树根到每个结点的路径长度的总和
3.常用于求解树结点与度之间关系的有:
总结点数=n0+n1+n2+……+nm
总分支点数=1n1+2n2+……+mnm
总结点数=总分支数+1
4.2 二叉树的概念
题目中涉及到的知识点(主要是我个人不熟悉知识点)
1.二叉排序树的插入和删除?
插入:
若二叉排序树为空,则插入结点作为根节点插入到空树中
否则,继续在左右子树中查找
树中已有,则不再插入
树中没有,则查找至某个叶子节点的左子树或右子树为空为止,则插入。结点应为该叶子结点的左孩子或右孩子
删除:
(1)被删除的结点是叶结点时,直接删除该结点
(2)被删除的结点只有左子树或者右子树,用其左子树或者右子树的结点替代他即可
举例:
(3)被删除的结点既有左子树又有右子树的时候
A.以其中序前驱值替换之(值替换),然后再删除该前驱结点。前驱是左子树中最大的结点
B.也可以用后继替换之,然后再删除该后继结点。后继是右子树中最小的结点。
选择AB的策略,主要是看树的高度,选择变换后树的高度最小的那一个。
2.树如何转化为二叉树?
将树转换成二叉树的步骤是:
(1)加线。就是在所有兄弟结点之间加一条连线;
(2)抹线。就是对树中的每个结点,只保留他与第一个孩子结点之间的连线,删除它与其它孩子结点之间的连线;
(3)旋转。就是以树的根结点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。
3.为什么在m叉树的情形下,结点i的第一个子女编号为j=(i-1)*m+2
4.3二叉树的遍历和线索二叉树
1.先序遍历的非递归算法?
思想:边遍历边打印,结点顺序是中左右
代码:
void PreOrder(BiTree T){
InitStack(S);BiTree p=T;//初始化栈,构造遍历指针P
while(p||!IsEmpty(S)){//当p不为空,S不为空时
if(p){//如果p不为空,就将该点输出,且拜访他的左子树
visit(p);
push(S,p);
p=p->lchid;
}
else{//如果p的左子树遍历完了,就开始回访以前的结点中哪些右子树是还没输出的,依次输出
pop(S,p);
p=p->rchild;
}
}
}
2.中序遍历的非递归算法:
思想:找到左子树最下边的结点,并同时要用栈保存根节点,因为要通过根节点来进入右子树。结点访问顺序,左中右。
代码:
void InOrder(BiTree T){
InitStack(S);BiTree p=T;
while(p||!IsEmpty(S)){
if(p){
push(S,p);
p=p->lchild;
}
else{
pop(S,p);
visit(p);
p=p->rchild;
}
}
}
3.后序遍历的非递归算法:(等题目具体涉及到了再来更新)
思想:要先访问完左孩子和右孩子之后才能访问根结点。如果根结点没有孩子,当然就直接输出;如果有,并且已经访问过左孩子和右孩子结点就可以访问根结点;如果根结点有孩子,但是孩子还没访问,就依次让右孩子和左孩子入栈,这样就保证了左孩子在右孩子之前访问。
代码:
void PostOrder(BiTree T){
InitStack(S);
BiTree p; //当前结点
BiTree pre=null;//前驱结点
push(T,S);//首先栈里面要存放根节点
while(!IsEmpty(S)){
p=S.top();//p的值为当前栈顶元素
if((p->lchild==null&&p->rchild==null)||
(pre!=null&&(pre==p->lchild||pre==p->rchild)){//如果p没有孩子结点,或者p的孩子结点已经访问过
visit(p);
pre=p;
}
else{
if(p->rchild!=null)push(S,p->rchild);//让右孩子先入栈
if(p->lchild!=null)push(S,p->lchild);
}
}
}
}
}
4.层次遍历:
思想:先进先出,先把根节点入队,然后出队并访问该结点,再把该结点的左子树和右子树结点入队。然后出队,对出队结点访问,如此反复。
代码:
void LeverOrder(BiTree T){
InitQueue(Q);
BiTree p;
EnQueue(Q,T);//将根结点入队
while(!IsEmpty(Q)){
DeQueue(Q,p);
visit(p);
if(p->lchild!=null){//如果左子树不为空,将其入队
EnQueue(Q,p->lchild);
}
if(p->rchild!=null){
EnQueue(Q,p->rchild);
}
}
}
5.可以唯一地确定一棵二叉树是哪些序列?
先序中序,中序后序,层序中序。
6.通过中序遍历对二叉树线索化的递归算法
思想:对二叉树的线索化,实质上就是遍历一次二叉树,只是在遍历的过程中,检查当前结点左右指针域是否为空,若为空,将它们改为指向前驱结点或后继结点的线索。
代码:
void InThread(ThreadTree &p,ThreadTree &pre){//p是当前结点,pre是上一个结点
if(p!=null){
InThread(p->lchild,pre);//递归访问左子树
if(p->lchild==null)//如果左孩子为空,就指向前驱结点
p->lchild=pre;
if(pre!=null&&pre->rchild==null)//如果前驱结点不为空,且前驱结点右孩子为空
pre-rchild=p;//就让前驱结点指向后继结点,即pre指向了p
pre=p;//记录前驱结点
InThread(p->rchild,pre);//递归访问右子树
}
}
7.线索二叉树的遍历
void InOrder(ThreadNode *T){//线索二叉树的中序遍历
for(ThreadNode *p=Firstnode(T);p!=null;p=Nextnode(p))
visit(p);
}
ThreadNode * Firstnode(ThreadNode *p){
while(p->Itag==0)p=p->lchild;//如果有左孩子,就一直找到最左的哪一个,也就是中序的第一个结点
return p;
}
ThreadNode * Nextnode(ThreadNode *p){
if(p->rtag==0) return FirstNode(p->rchild);//如果没有线索,就找到右孩子树的最左结点
else return p->rchild;//否则就是有线索,返回后继元素即可
}
8.线索二叉树是一种什么结构?
物理结构。
9.为什么后序线索二叉书中求后序后继问题仍不能有效解决。
随便举个例子,找一个不是根节点且带有右孩子的结点,当求后序后继结点时,由于右孩子不为空,所以根本无法找到后继结点。
10.后序遍历为什么需要栈的支持?
因为当结点x的右孩子不为空时,右指针无法指向其后继,所以通过指针可能无法遍历整棵树。所以还是需要栈的支持的。
4.4 树、森林
4.4.1 树的存储结构
4.4.2 树、森林与二叉树的转换
二叉树=>树:左孩,右孩连双亲,去掉原来右孩线
树=>二叉树:兄弟相连留长子,绕根结点旋转45°
森林=>二叉树:树变二叉根相连
二叉树=>森林:去掉全部右孩线,二叉再还原
二叉树转换成树的时候,记住二叉树的结构是左孩子右兄弟
4.4.3 树和森林的遍历
4.5 树与二叉树的应用
4.5.1 二叉排序树
4.5.2 平衡二叉树
4.5.3 哈夫曼树和哈夫曼编码
1.为什么哈夫曼编码能够保证是前缀编码?
因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其他叶结点编码的前缀
2.为什么哈夫曼编码能够保证字符编码总长最短?
因为哈夫曼树的带权路径长度最短,故字符编码的总长最短
性质一:哈夫曼编码是前缀码
性质二:哈夫曼编码是最优前缀码
注意:哈夫曼树并不一定是二叉树,度为m的哈夫曼树只有度为m和度为0的结点。
3.若度为m的哈夫曼树中,叶子结点个数为n,则非叶子结点的个数为多少?
首先回忆一下公式:
总结点数=n0+n1+n2+……+nm
总分支点数=1n1+2n2+……+mnm
总结点数=总分支数+1
回答:
设结点总数为S, 度为m的结点个数设为x
S=x+n;
具有S个结点的哈夫曼树有S-1条分支
S-1=xm
x+n-1=xm
x=(m-1)/(n-1)
4.6 树的应用题总结
1.什么是最佳二叉排序树?怎么构造他?
最佳二叉排序树就是指高度最小的二叉排序树,构造方法就是把给定的序列从小到大排序,然后用二分法,每次去中间值。当然取中间偏左的值还是中间偏右的值都可以。(具体实现比较简单,大致看一遍就可以了,此处不举例)
2.什么是大根堆?
大根堆要求根节点的关键字值大于等于左右子女的关键字值,且大根堆是一棵完全二叉树