考试复习整理
最近数据结构课要考试了,这学期基本没去过数据结构课堂,咳咳,拿出书和课件来翻翻,不少内容都比CLRS繁琐而不清晰。
简单整理了一些要点,发现这些东西或许还是有些分享价值的。。。虽说大多非常水。
稀疏矩阵
-
非零元素/(m * n) < 0.05 可认为是稀疏的,当然实际上是一个经验判断,这个数值仅供参考而已
-
采用三元组进行存储
整体是一个数组,每个元素是一个struct,成员包括value、行、列 -
转置:
一般转置算法:原矩阵为A,转置后为B:
- 初始化B,B.rows=A.cols;B.cols=A.rows;
j=0 //用来扫描B的index
for(k : 0 to A.cols) //按照原矩阵的列扫描
for(i : 0 to terms) //terms是矩阵中非零元素的个数
if(A.matrix[i].col == k)
B.matrix[j].row=A.matrix[i].col
B.matrix[j].col=A.matrix[i].row
B.matrix[j].value=A.matrix[i].value
j++-
快速转置(类似于桶排序)
前一个转置是很慢的,时间复杂度高达O(cols*terms)
我们可以先扫描A,确定A中每一列有几个元素,
如: col 0 1 2 3 4 5
元素个数 1 1 1 3 0 2则此时可以计算出一个数组help,第i个元素代表A中第i列的元素放在B的help[i]位置上
help 0 1 2 3 6 6for(i : 0 to terms)
pos=help[ A.matrix[i].col ]
B.matrix[pos].col = A.matrix[i].row
B.matrix[pos].row = A.matrix[i].col
B.matrix[pos].value = A.matrix[i].value
help[A.matrix[i].col]++
//这里让help数组中对应元素自增;这样遇到原A中和此元素同一列的元素时可以自动放在它的下一个位置
广义表
-
这个数据结构在C++ 中用的不多,但是在scheme中已经见识到了这样一个 闭包结构的威力,看过SICP的孩子们看到广义表肯定倍感亲切呀。
比如随手写一个: (cons a (cons b (cons c nil))) 就是一个链表, (cons (list a b ) (list c d)) 就是一个二叉树 -
深度:括号的嵌套层次
-
长度:最外层的子表个数
-
example: depth length
- A () 1 0
- B (a b) 1 2
- C (a b (d e f) ) 2 3
- D (A B C) 3 3
- E (B D) 4 2
- F (h F) 乄 2
*也就是说,一个广义表的深度=它的所有子表里面深度最大的一个的深度+1而广义表的长度则不管子表的内容,子表和原子作为同样看待,看有几个
利用栈模拟实现二叉树的非递归遍历
这是一个非常经典的问题,借考试之故做个整理也是极好的
前序遍历
前序的改写最为容易,因为前序的右子树遍历是尾递归,而左子树遍历也接近尾递归,可以轻易的写为迭代而不借助栈
比如:
void preorder(treenode *node){
while(node!=NULL){
if(node->left!=NULL){
node=node->left;
break;
}
node=node->right;
}
这样的代码对于我们理解栈模拟没有任何用处,它只是用到了尾递归优化技巧,并且对于我们写出中序和后序也没有帮助
思考下面的方法:
void stack_preorder(treenode *node){
stack s;
while(true){
while(node!=NULL){
visit(node);
s.push(node->right);
node=node->left;
}
if(s is empty) break;
node=s.pop();
}
}
当然,这里的两侧while可以写成一层:
void stack_preorder(treenode *node){
stack s;
while(true){
if(node!=NULL){
visit(node);
s.push(node->right);
node=node->left;
}
else if(s is empty) break;
else node=s.pop();
}
}
中序遍历
前序的后一种方法很容易让我们写出利用栈模拟的中序遍历算法:
void stack_inorder(treenode *node){
stack s;
while(true){
if(node != NULL){
s.push(node);
node=node->left;
}
else if(s is empty)
break;
else {
treenode *p=s.pop();
visit(p);
node=node->right;
}
}
}
到这里我们需要思考下一个问题,栈模拟是在帮助我们记住返回的路径,有没有办法不用栈呢?
(之前有篇模版二叉树的博文里面的operator<< 实际上就是一个非递归不用栈的二叉树遍历)
void inorder(treenode *node){
while(true){
if(node->left!=NULL){
node=node->left;
}
else {
visit(p);
while(node->right==NULL)
{
node=node->succ();
if(node==NULL)
break;
else visit(node);
}
node=node->right;
}
}
}
解释一下,首先循环开始:
从当前开始不断的向左,直至最左结点;
访问最左结点;
最左的结点没有右孩子:
访问它中序下的后继;
循环这一过程
访问它的右孩子(此时将右孩子作为和根结点一样的地位,进行遍历) (实际上中序的右子树遍历是直接尾递归优化来的,很容易理解)
后序遍历
后序遍历就稍微麻烦一些,因为它的左子树和右子树遍历均不是尾递归。
这时候我们要解决一个问题,从栈中退出的时候我们接下来要访问什么?
我们无法确定是原递归的左子树调用还是右子树调用,因此需要添加一个标记。
标记取为enum 的两个常量:from_left_to_right from_right_to_root分别对应当前结点是从左子树遍历中退出应该去右子树、以及遍历完右子树应该访问它自身两种情况
void stack_postorder(treenode *node){
stack s;
s.push(node);
while(s is not empty){
while(node->left != NULL){
node=node->left;
s.push(node);
}
while(true){
node=s.pop();
if(node.tag== from_left_to_rigth){
s.push(node);
node.tag=frome_right_to_root;
node=node->right;
break;
}
else if(node.tag==frome_right_to_root){
visit(node);
}
}
}
}
二叉树计数和退栈可能数
- n个元素能够组成多少个不同的二叉树?
- 给定n个元素的进栈顺序,有多少种合法的出栈顺序?
上面两个问题实际上是等价的,先来看第二个问题:
- 首先我们要明确什么样的出栈顺序不合法:
如入栈顺序为i,j,k,出栈顺序为pk < pi < pj,则不合法,
比如 123 入栈,则312不合法
- 考虑n个元素 1 2 3 ……n 进栈,合法的出栈顺序数满足下列递推:
f(n)= f(n-1) * f(0) + f(n-2) * f(1) + f(n-3) * f(2) + …… + f(0)f(n-1)
解释一下:
左边:f(n)表示1 2 3 …… n的入栈顺序下的出栈可能的数目
右边: 我们按照最后一个出栈元素的不同来分类:
1. 最后一个出栈的是n ,则此时出栈的种类数=f(n-1)
2. 最后一个出栈的是n-1,则此时出栈的种类数等于f(n-1)f(1)
3. 依次类推,直至f(0)*f(n-1)
上述的f(n)实际上就是catalan数,其通项公式为C(n,2n)/(n+1)
而二叉树的种类数也是上面的catalan数