您的位置:首页 > 理论基础 > 数据结构算法

数据结构——浙大网易云课堂记录(一)树(上篇)

2018-03-02 23:26 405 查看
前菜:查找
顺序查找

二分查找
二分查找判定树

树的一些概念

树的表示

课后题

二叉树
几种特殊二叉树

二叉树性质

二叉树存储结构

二叉树的递归遍历(以链式存储为例)
先序遍历

中序遍历

后序遍历

递归遍历时候的路径

二叉树的非递归遍历
中序遍历

先序遍历

后续遍历

层序遍历

遍历二叉树的应用
输出叶子节点

求二叉树的高度

由两种遍历序列确定二叉树

课堂链接

前菜:查找

根据某个给定的关键字K,从集合R中找出与K相同的记录。

静态查找:集合中记录是固定的,没有删除和插入操作。

动态查找:有插入、删除操作。

顺序查找

e.g. 静态查找例子:表如下,表头指针Tbl,
Tbl -> Length = 10
表示表的长度。查找时候从表尾,从后往前查找,返回元素K的下标。表头(下标为0)不存放数据。



常规写法是这样:

/* 在Tbl
到Tbl[1]中查找元素K的位置 */
int SequentialSearch (StaticTable *Tbl, ElementType K){
int i; // now index
for(i = Tbl -> Length; i > 0 && Tbl -> Element[i] != K; i--);
return i;
}


注意到每次for循环中总要判断
i > 0
,这是为了防止查找越界。那么换一种思路,如果查找到最后一个元素,其实也是到达查找的边界。把下标为0的元素的值设为K,最终总会得到
Tbl -> Element[i] = K
,i的值若为0,则说明遍历了整个列表都没查找到K;若不为0,则返回位置i。这样可以减少循环中的一次比较次数。

int SequentialSearch (StaticTable *Tbl, ElementType K){
int i; // now index
Tbl -> Element[0] = K; // Sentinel element
for(i = Tbl -> Length; i > 0 && Tbl -> Element[i] != K; i--);
return i; // returns: the index of Element K ; 0 if K is no found
}


这种查找方法,平均查找次数
N/2
,时间复杂度
O(N)


二分查找

前提:数据有序存放。





代码如下:

int BinarySearch( StaticTable *Tbl, ElementType K) {
int left, right, mid, NotFound = -1;
// 初始值
left = 1;
right = Tbl -> length;

while (left <= right) {
mid = ( left + right) / 2; // 计算中间元素坐标
if (Tbl -> Element[mid] < K){
left = mid + 1;
}else if (Tbl -> Element[mid] > k){
right = mid - 1;
}else{
return mid
}
}
return NotFound;
}


算法复杂度
O(logN)


[Note]: 如果
left
right
的更新由
left = mid+1
right = mid-1
改为
left = mid
right = mid
,结果会出错,这跟
while
里面的条件有关的。本例中条件为
left > right
时(查找失败)退出查找,假如在三个数

6 9 10


中查找8,为了方便,直接用
left
mid
right
代表
6
9
10


初始:

left = 6
right = 10


进入比较

mid = 9
9 > 8  -->left不变         left = 6
10 > 8 -->right变为mid的值  right = 9

mid = 6
6 < 8  --> left变为mid的值 left = 6
6 < 10 --> right不变       right = 9


此时
left
right
相邻,并不破坏while循环规则
left <= right
,因此进入死循环,永远在6和9之间查找8(当然是找不到的了)。

为了避免这种情况,每次
left
right
都会避开
mid
值,哪怕最后只剩两个元素时候,如果mid恰好是
left
right
中的一个,则
left+1
right+1
中有一个会进行,总能找到
mid
;如果mid都不等于
left
right
中的任一个,则
left+1
right+1
都会执行,最终带来
left > right
的结果,结束循环。

若想用
left = mid
right = mid
来作为更新条件,则需改写

while (left + 1 < right) {
mid = ( left + right) / 2; // 计算中间元素坐标
if (Tbl -> Element[mid] < K){
left = mid;
}else {
right = mid - 1;
}
}
// 剩下两个元素,需要判断
if (Tbl -> Element[left] == K) return left;
if (Tbl -> Element[right] == K) return right;
return NotFound;


二分查找判定树

假设查找11个元素。如果查找元素在位置6,则只需要查找一次;如果查找位置在4,则分别在1-6之间,3-6之间,4-6之间查找,
mid
的值以此更新:
6 -> 3 -> 4
,在判定数上可以找到该路径。二分查找判定树性质如下:(注:
ASL
平均成功查找次数



启示:用树来存放数据,查删改可以快很多。

树的一些概念

一棵有N个节点的数,它的边有
N-1
条。树是连通N个节点所用边最少的数据结构。

概念:

节点的度(Degree):节点的子树个数

树的度:树所有节点的最大度数

叶节点(Leaf):度数为0的节点

子节点(Child)、父节点(Father)、兄弟节点(Sibling)、祖先节点(Ancestor)、子孙节点(Descendant)

路和路径长度:节点n1到nk的路径为一个节点序列
n1, n2, ... ,nk
,这条路径上边的长度为该路径长度。

节点的层次(Level):根节点层次为1

树的高度:最深层次

树的表示



用数组:太复杂

用链表:浪费空间

所以,用另一种方法表示



把脖子向左歪45度,看到一棵很漂亮的二叉树



课后题

在分量1~11的数组中按从小到大顺序存放11个元素,如果进行二分查找,查找次数最少的元素位于什么位置?

A.1

B.5

C.6

D.11

一棵度为 m的树有n个节点。若每个节点直接用m个链指向相应的儿子,则表示这个树所需要的总空间是n*(m+1) (假定每个链以及表示节点的数据域都是一个单位空间)。当采用儿子/兄弟(First Child/Next Sibling)表示法时,所需的总空间是:

A.3n

B.2n

C.n*m

D.n*(m-1)

二叉树

几种特殊二叉树

斜二叉树(Skewed Binary Tree)

完美二叉树(Perfect Binary Tree)/满二叉树(Full Binary Tree)

完全二叉树(Complete Binary Tree)

二叉树性质

i
层最多有
2^(i-1)
个节点

深度为
k
的二叉树最大节点总数为
2^k - 1
(每层累加)

记非空二叉树中,度数为0、1、2的节点分别为
n0、n1、n2
,则有
n0 = n2 + 1


pf:

从树的根节点开始往下看,下方的边看做是上方的节点的贡献

节点总数=边的总数+1节点总数=边的总数+1

n0+n1+n2=0∗n0+1∗n1+2∗n2n0+n1+n2=0∗n0+1∗n1+2∗n2

化简可得结果

二叉树存储结构

完全二叉树的顺序存储



链表存储

typedef struct TreeNode *BinTree;
typedef BinTree Position;
struct TreeNode {
ElementType Data;
BinTree Left;
BinTree Right;
}




二叉树的递归遍历(以链式存储为例)

先序遍历

先访问根节点

先序遍历左子树

先序遍历右子树

void PreOrderTraversal( BinTree BT) {
if (BT) {
printf("%d", BT -> Data);
PreOrderTraversal( BT -> Left);
PreOrderTraversal( BT -> Right);
}
}




中序遍历

中序遍历左子树

访问根节点

中序遍历右子树

void InOrderTraversal( BinTree BT) {
if (BT) {
InOrderTraversal( BT -> Left);
printf("%d", BT -> Data);
InOrderTraversal( BT -> Right);
}
}




后序遍历

后序遍历左子树

后序遍历右子树

访问根节点

void PostOrderTraversal( BinTree BT) {
if (BT) {
PostOrderTraversal( BT -> Left);
PostOrderTraversal( BT -> Right);
printf("%d", BT -> Data);
}
}




递归遍历时候的路径

按图中路径,每个节点会被遇到三次,在不同相遇时刻访问该节点,对应不同的遍历方式。



二叉树的非递归遍历

思路:遇到节点的时候,不该访问的话就压入栈,访问便弹出。

中序遍历

遇到一个节点,就把它压栈,并去遍历它的左子树

左子树遍历结束后,从栈顶弹出这个节点并访问它

按其右指针去中序遍历右子树

void InOrderTraversal(BinTree BT){
BinTree T = BT;
Stack S = CreateStack(MAXZIZE); // 创建并初始化堆栈
while(T || !isEmpty(S)){
while(T){ // 一直向左,将沿途节点入栈
Push(S, T);
T = T -> Left;
}
while(!isEmpty(S)){
T = Pop(S); // 节点弹出堆栈
printf("%d", T -> Data); // 访问节点
T = T -> Right; // 转向右子树
}
}
}


先序遍历

遇到一个节点,访问它,并去

遍历它的左子树

先序遍历右子树

void PreOrderTraversal(BinTree BT){
BinTree T = BT;
Stack S = CreateStack(MAXZIZE);
while(T || !isEmpty(S)){
while(T){
printf("%d", T -> Data);
Push(T, S);
T = T -> Left;
}
while(!isEmpty(S)){
T = Pop(S);
T = T -> Right;
}
}
}


后续遍历

遇到一个节点,压栈

如果有左子树,访问它的左子树,否则访问右子树。

如果没有左子树,也没有右子树,访问这个节点。

void PostOrderTraversal(BinTree BT){
BinTree T = BT;
Stack S = CreateStack(MAXZIZE);
while(T || !isEmpty(S)){
while(T){
Push(T, S);
T = T -> Left;
}
T = Pop(S); // 到达最左端
if(!T -> Right && !isEmpty(S)){
printf("%d", T -> Data);
}else {
T = T -> Right;
}
}
}


层序遍历

遇到一个节点,如果访问它的左儿子,不保存当前位置信息,那右儿子就会“丢失”。此时的思路是把该父节点,或是右儿子,通过栈/队列存起来。

层序遍历基本过程:根节点先入队,然后

从队列中取出一个元素

访问这个元素

如果这个元素的左、右儿子非空,将左、右儿子按顺序入队

void LevelOrderTraversal(BinTree BT){
Queue Q;
Q = CreateQueue(MaxSize);
AddQ(Q, BT); // 根节点入队
while(!isEmpty(Q)){
T = Delete(Q);
printf("%d",T -> Data );
if(T -> Left) AddQ(T -> Left);
if(T -> Right) Push(T -> Right);
}
}




遍历二叉树的应用

输出叶子节点

在访问节点过程中增加判断,若左右子树都为空,输出该节点。

void PreOrderPrintfLeave(BinTree BT){
if(BT){
if(!T -> Left && !T -> Right){
printf('%d', T -> Data);
}
PreOrderPrintfLeave(T -> Left);
PreOrderPrintfLeave(T -> Right);
}
}


求二叉树的高度

分别求左子树和右子树的高度,取两者中最大值,加一。

int PostOrderGetHeight(BinTree BT){
int HL, HR, MaxH;
if(BT){
HL = PostOrderGetHeight(BT -> Left);
HR = PostOrderGetHeight(BT -> Right);
MaxH = (HL > HR) ? HL : HR;
return MaxH + 1;
}else {
return 0;
}
}


由两种遍历序列确定二叉树

对一棵所有节点没有左儿子(或没有右儿子),它的前序、后序遍历结果是一样的。所以要由遍历序列确定一棵树,还需要加上中序遍历。



[Note]:找根节点:先序的根节点在(子)序列的第一个,后序的根节点在(子)序列的最后一个。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐