前菜:查找
根据某个给定的关键字K,从集合R中找出与K相同的记录。
- 静态查找:集合中记录是固定的,没有删除和插入操作。
- 动态查找:有插入、删除操作。
顺序查找
e.g. 静态查找例子:表如下,表头指针Tbl,
Tbl -> Length = 10表示表的长度。查找时候从表尾,从后往前查找,返回元素K的下标。表头(下标为0)不存放数据。
常规写法是这样:
/* 在Tbl[n]到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∗n2 n 0 + n 1 + n 2 = 0 ∗ n 0 + 1 ∗ n 1 + 2 ∗ n 2
化简可得结果
二叉树存储结构
- 完全二叉树的顺序存储
- 链表存储
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]:找根节点:先序的根节点在(子)序列的第一个,后序的根节点在(子)序列的最后一个。
2123



被折叠的 条评论
为什么被折叠?



