C语言二叉树基本操作

二叉树结构: 

 

 输入#符号表示节点结束:

重要性质:

1.在二叉树的第k层上至多有2^{k-1}个结点

2.深度为k的二叉树至多有2^{k}-1个节点(此时为满二叉树)

3. 

n=n_{0}+n_{1}+n_{2} (1)

一个非根节点的节点,总是会有一个父节点挂着它,所以分支的数量B等于非根节点的数量,所以n=B+1=n_{1}+2n_{2}+1 (2)

(1)式-(2)式得到n_{0}=n_{2}+1

4.有n个节点的二叉树深度为\left \lfloor log_{2}n \right \rfloor + 1

5.完全二叉树中编号为i的节点,左孩子编号为2i,右孩子编号为2i+1。2i大于节点总数n则无左孩子,2i+1大于右孩子则无右孩子。

1. 先序遍历,中序遍历,后序遍历

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

typedef struct BiTNode
{
    char data;
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

void CreateBiTree(BiTree &T)
{
    char data;
    scanf("%c", &data);
    while (getchar() != '\n')
    {
        ;
    } // 清除scanf缓存区
    if (data != '#')
    {
        T = (struct BiTNode *)malloc(sizeof(struct BiTNode));
        T->lchild = NULL;
        T->rchild = NULL;
        T->data = data;
        CreateBiTree(T->lchild);
        CreateBiTree(T->rchild);
    } else {
        T = NULL; // 孩子节点为空
    }
}

void CenterInOrderTraverse(BiTree &T)
{
    if (T)
    {
        CenterInOrderTraverse(T->lchild);
        printf("节点字符%c\n", T->data);
        CenterInOrderTraverse(T->rchild);
    }
}

void FrontInOrderTraverse(BiTree &T)
{
    if (T)
    {
        printf("节点字符%c\n", T->data);
        FrontInOrderTraverse(T->lchild);
        FrontInOrderTraverse(T->rchild);
    }
}

void EndInOrderTraverse(BiTree &T)
{
    if (T)
    {
        EndInOrderTraverse(T->lchild);
        EndInOrderTraverse(T->rchild);
        printf("节点字符%c\n", T->data);
    }
}

int main()
{
    BiTree T;
    CreateBiTree(T);
    //        a
    //     b      d
    //    c #   e   f
    //   # #   # # # #
    //   输入abc###de##f##
    // CenterInOrderTraverse(T); // cbaedf
    FrontInOrderTraverse(T); // abcdef
    // EndInOrderTraverse(T); // cbefda
    return 0;
}

输入:abc###de##f##,生成二叉树。

  • 中序遍历:为cbaedf。 
  • 先序遍历:abcdef。
  • 后序遍历:cbefda。

 2. 使用链表栈实现非递归的中序遍历

链表栈里的数据保存的是二叉树的节点BiTreeNode结构主要步骤:

【算法步骤】
心初始化一个空栈 s, 指针p指向根结点。
@申请一个结点空间q, 用来存放栈顶弹出的元素。
@当p非空或者栈S非空时,循环执行以下操作:
• 如果p非空,则将p进栈,p指向该结点的左孩子;
• 如果p为空,则 弹出栈顶元素并访间,将p指向该结点的右孩子。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
// 中序遍历的非递归算法

// 二叉树
typedef struct BiTreeNode
{
    char data;
    struct BiTreeNode *lchild, *rchild;
} BiTreeNode, *BiTree;

// 链表栈
typedef struct StackNode {
    struct BiTreeNode *tree;
    struct StackNode *next;
} StackNode, *LinkStack;

int isEmptyStack(LinkStack &S) {
    if (S == NULL) {
        return 1; // 空栈
    } else {
        return 0;
    }
}

void InitBiTree(BiTree &T)
{
    char ch;
    scanf("%c", &ch);
    while (getchar() != '\n')
    {
        ;
    }
    if (ch != '#')
    {
        T = (BiTreeNode *)malloc(sizeof(struct BiTreeNode));
        T->data = ch;
        T->lchild = NULL;
        T->rchild = NULL;
        InitBiTree(T->lchild);
        InitBiTree(T->rchild);
    }
}

// 不需要初始化空节点,直接为空
void InitStack(LinkStack &S) {
    S = NULL;
}

void PushStack(LinkStack &S, BiTree &T) {
    struct StackNode *p = (struct StackNode *)malloc(sizeof(struct StackNode));
    p->tree = T;
    p->next = S; // S代表地址;
    S = p; // p也是地址
}

void PopStack(LinkStack &S, BiTree &T) {
    if (S) {
        T = S->tree;
        S = S->next;
    }
}

// 中序遍历的非递归算法
void CenterTraverseByStack(LinkStack &S, BiTree T) {
    BiTree p = T; // 不要直接操作p里的值,参数一般为BiTree T而不是BiTree &T。
    while (p || !isEmptyStack(S)) { // p不存在,弹出栈顶元素访问,下次循环栈顶的右孩子
        if(p) {
            PushStack(S, p); // 将数据入栈,指针p指向
            p = p->lchild;
        } else {

            BiTree q;
            PopStack(S, q);
            printf("%c ", q->data);
            p = q->rchild;
        }
    }
}

void CenterTraverse(BiTree &T)
{
    if (T)
    {
        CenterTraverse(T->lchild);
        printf("%c ", T->data);
        CenterTraverse(T->rchild);
    }
}

int main()
{
    BiTree T;
    //        a
    //     b      d
    //    c #   e   f
    //   # #   # # # #
    //   输入abc###de##f##
    puts("---输入abc###de##f##创建二叉树---");
    InitBiTree(T);
    puts("---中序遍历---");
    CenterTraverse(T); // c b a e d f 
    LinkStack S;
    InitStack(S);
    puts("---使用栈遍历树---");
    CenterTraverseByStack(S, T);
    puts("---end---");
    return 0;
}

3. 复制二叉树 

将二叉树进行遍历,然后将里面的每个节点复制到另一个空树中,以javascript为例:

JavaScript里不要对形参直接等于号操作,那样当前循环的等于号操作改变不了上一个循环调用的实参里的成员。C语言里我们可以传入引用&NewT,然后NewT = xxx影响实参:

void Copy(BiTree T, BiTree &NewT) {
    if (T)
    {
        NewT = (struct BiTNode *)malloc(sizeof(struct BiTNode));
        NewT->data = T->data; 
        Copy(T->lchild, NewT->lchild);
        Copy(T->rchild, NewT->rchild);
    } else {
        NewT = NULL;
    }
}

4.计算深度

int Depth(BiTree T) {
    if (T) {
        int m = Depth(T->lchild);
        int n = Depth(T->rchild);
        if (m > n) {
            return m + 1;
        } else return n + 1;
    } else return 0;
}

5.统计节点个数 

int NodeCount(BiTree T)
{
    if (T)
    {
        return NodeCount(T->lchild) + NodeCount(T->rchild) + 1; 
    }
    else
        return 0;
}

6.根据中序和后序生成二叉树 

在学习线索二叉树之前,先看看如何根据中序和后序生成二叉树,不然每次使用scanf去一个个输入太麻烦,对于后序遍历而言,最后一个节点就是二叉树(整个树或者子树)的根节点,再根据中序的特点:左子树都在根节点左边,右子树都在根节点右边进行递归遍历。另外不管是哪种遍历方式左孩子总是在右孩子前面。

        所以假设有10个节点,根节点在第4个位置,那么中序遍历中,前3个是左子树,后面6个是右子树。而在后序遍历中,前3个是左子树,第4-9个是右子树,最后一个是根节点。此时中序遍历这10个节点,根节点右子树上的所有节点不可能在后序遍历中的前3位出现。因为左子树总是在右子树的前面遍历。

考虑二叉树:

    +             
 a       *       

       b    -
          c   d

定义变量:

char center[] = "a+b*c-d"; // 中序遍历

char right[] = "abcd-*+"; // 后续遍历

然后期望是根据这两个变量生成二叉树,节省一个个输入字符的时间。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

// 中序线索二叉树
typedef struct BiThrNode
{
    char data;
    struct BiThrNode *lchild, *rchild;
    int Ltag; // 1:前驱 0:左节点
    int RTag; // 1:后驱 0:右节点
} BiThrNode, *BiThrTree;

// a+b*(c-d)
//     +             
//   a    *       
//       b   -
//         c   d

void printThread(BiThrTree &p)
{
    if (p)
    {
        printThread(p->lchild);
        printf("%c ", p->data);
        printThread(p->rchild);
    }
}

// 根据中序遍历和后续遍历生成二叉树
void rebuild(BiThrTree &tree, char* start, char* end, int centerLength) {
    // 子树的长度
    if (centerLength <= 0) {
        return;
    } else {
        BiThrTree current = (struct BiThrNode *)malloc(sizeof(struct BiThrNode));
        current->data = *(end + centerLength - 1);
        current->lchild = NULL;
        current->rchild = NULL;
        current->Ltag = 0;
        current->RTag = 0;
        tree = current;
        // end是中序遍历的数组,最后一个是子树的根节点
        int rootIndex = 0;
        while (rootIndex < centerLength && start[rootIndex] != *(end + centerLength - 1))
        {
            rootIndex++; // 找到根节点的位置
        }
        rebuild(tree->lchild, start, end, rootIndex);
        // printf("%c  %d  \n", *(start + rootIndex + 1),  centerLength - rootIndex - 1);
        rebuild(tree->rchild, start + rootIndex + 1, end + rootIndex, centerLength - rootIndex - 1);
    }

}

int main()
{
    BiThrTree p;
// +a##*b##-c##d##
    char center[] = "a+b*c-d"; // 中序遍历
    char right[] = "abcd-*+"; // 后续遍历
    rebuild(p, center, right, sizeof(right) / sizeof(right[0]) - 1);
    printThread(p);
    return 0;
}

7. 线索二叉树

线索二叉树的优点是遍历的时间复杂度,实现的原理是遍历的过程中如果没有左孩子,则lchild指向前驱节点, 如果没有右孩子则指向后驱节点,因此还需要引入两个标识LTag和RTag,1表示有左孩子和右孩子,0表示没有孩子。按照遍历的方式,分为前序、中序和后序线索二叉树:

每次遍历的节点保留到pre变量中,这样遍历时可以访问到上一个前驱节点:

BiThrTree pre = NULL; // 指向上一个访问的节点

void InTheading(BiThrTree &p) {
    if(p) {
        InTheading(p->lchild);
        if(!p->lchild) {
            p->Ltag = 1;
            p->lchild = pre;
        } else {
            p->Ltag = 0; // 有左孩子为0,没有左孩子为1,指向前一个节点
        }
        if (!pre->rchild) {
            pre->RTag = 1;
            pre->rchild = p; // 没有右孩子,指向下一个节点
        } else {
            p->RTag = 0;
        }
        pre = p;
        InTheading(p->rchild);

    }
}

 8. 树的存储

 9. 图的存储

领接矩阵、邻接表和十字链表。

10. DFS(广度优先搜索)和BFS (深度优先搜索) js版本

如果安装了nodejs,使用 node xxx.js直接运行js代码,或者粘贴到浏览器F12控制台里。

//          1
//       2   3   4
//     5    6 7   8
//   9    10
const tree = {
    data: 1,
    next: [{
        data: 2,
        next: [{
            data: 5,
            next: [{
                data: 9,
                next: null
            }]
        }]
    },
    {
        data: 3,
        next: [{
            data: 6,
            next: [{
                data: 10,
                next: null
            }],
        }, {
            data: 7,
            next: null
        }]
    },
    {
        data: 4,
        next: [null, {
            data: 8,
            next: null
        }]
    }
    ]
}
// 深度优先搜索,将节点放在一个栈里。因为是先一直往下找,所以同一层次的节点从右到左的入栈,然后栈顶出栈访问并压入它的子节点。
// 对于图而言,还需要构造一个visited数组,这样一个节点如果有多个父节点,只会访问一次。
function dfs(tree) {
    const stack = [];
    stack.push(tree); // 根节点入栈
    while(stack.length) {
        const currentNode = stack.pop(); // 提取数组最后一个
        console.log(currentNode.data);
        if (currentNode.next && currentNode.next.length) {
            let i = currentNode.next.length;
            // 倒序入栈,不要currentNode.next.reverse().forEach,这样会改变原有tree的结构。
            while(i) {
                currentNode.next[i-1] && stack.push(currentNode.next[i-1]);
                i--;
            }
        }
    }
}
console.log("---深度搜索---");
dfs(tree);
// 广度搜索,构造一个队列,将子节点按从左到右入队,然后出队访问
function bfs(tree) {
    const stack = [];
    stack.push(tree); // 根节点入栈
    while(stack.length) {
        const currentNode = stack.shift(); // 提取数组第一个
        console.log(currentNode.data);
        if (currentNode.next && currentNode.next.length) {
            currentNode.next.forEach(item => {
                if(item) { // 非空节点
                    stack.push(item);
                }
            });
        }
    }
}
console.log("---广度搜索---");
bfs(tree);

思路其实很简单,用一个数组模拟栈或者队列。唯一需要注意的是形参是复杂类型的对象,不用调用reverse()方法去遍历入栈,这样要遍历的树结构会改变。不过这样一来交换二叉树的左右孩子是不是思路就有了?

11. BP和KMP算法

next[j]表示去匹配j个字符,前面j-1个相同,第j个不同时,要回溯到第next[j]个元素(前面的next[j]-1个元素相等),然后拿next[j]位置去和第j个元素匹配。如next[5]=3,意味着匹配到第5个字符时,发现不同了,则把字符串往右移位直到第三个位置,此时前两个位置t1t2=t3t4,所以可以拿t3去和母串的第5个位置去匹配,这时的结果就依赖于母串的值,而next[j]是可以通过递归逐级得到,因为计算next[j]总是意味着前面j-1个元素是匹配上的。

然后使用书上的例子进行验证:

// BMP算法
// 主串 ababcabcacbab
// 子串 abcac
//       abcac 不行,因为和abcac和abcab不等。取出主串的abca和子串的abc比较,由于匹配到第5个字符不等,前面4个字符相等的,
//             所以可以看做是子串abca部分和去掉末尾以后的abc对比,因为abca同时是主串和子串序列。这样转化的好处是需要往前移到哪个字符对比只和子串自身有关。
 
//       abca 
//        abc 不行,因为c不等于a,这里就已经有规律了,即往前匹配时,先要保证找到的字符等于末尾,即a的前面一个a
//          a 此时只有一个字符a,这样就匹配上了1个字符,避免了还去从第4个字符开始重新匹配。实际上假设前面还有字符,想要匹配上只有可能是c,即ca去和abca的最后两位匹配。
//      这样的例子很容易推导出: 子串等于cabcax,主串为cabcabcax。第一次匹配卡在了主串的第6个字符b时,则前5个字符cabca和ca匹配上。这样由于第4,5位是ca对上了
//      所以下一次匹配是从子串的第三个字符b和主串的第6个字符b对比,当然如果主串的第6个字符不是b,那实际上就从ca开始往前找了,由于c不等于a,找不到相同的部分
//      所以这时就只能从第一个字符c和主串的第6个字符比(例:子串是cabcax,主串是cabcadcabcax,此时主串第6个字符不是b,要从ca往前找)。
 
// 将子串需要往前移的index下标放在next数组里记录,如abaabc就定义next是6维数组,next[6]表示第6个字符c匹配不上时,要返回的位置。
// 这时候考虑abaab,和ab匹配上,所以next[6]= 3,表示ab不用匹配了,从第三个字符a开始。又比如next[1]表示第一个字符就匹配不上,那只能拿子串的第一个字符重头开始匹配主串的下一个字符。
// next[j]计算思路方式很简单,如果用T表示子串,那么就是往前找到第一个和T[j-1]相等的位置k,然后还需要保证k之前的k-1个字符都和T[j-2]之前的k-1个字符相等。
// 因此next[j+1]=next[101]=k=10,其含义表示:1-10个字和91到100个字相等,即T[j-1]=T[k-1]。求next[j+1]是一个递归的过程,设next[j]=k。如果T[j-1]=T[k],则带上前面k-1个数就相等。
// 此时next[j+1]= next[j]+1。如果S中第j个数不等于第k个数,就去前面找匹配的字符,这时候去找next[k]= t,这时候如果第j个数T[j-1]= T[t-1],说明前面t-1个字带上第t个字T[t-1],一共t个字符是匹配的
// 此时next[j+1] = t + 1。
 
const str1 = "abaabcac";
// const str = "aba";
function get_next(T) {
    let i = 2;
    let next = [];
    next[1] = 0; // 第一个匹配不上,应该拿子串的第一个字符去匹配主串的下一个字符了。
    j = 0; // 保存next[i]的值。
    while (i <= T.length) {
        if (j == 0) {
            next[i] = j + 1; // 第一次进来的时候next[2] = 1; 子串的第二个字符对不上,表示下一次拿子串的第一个去匹配主串的当前字符。
            i++;
            j++;
        } else if(T[i-2] == T[j-1]) {
            // 求next[i]时,考虑第i-1个数和第next[i-1]=k个数是否相等
            next[i] = j + 1;
            j++;
            i++;
        } else {
            j = next[j];
        }
    }
    // next.shift(); // 去掉第一个即可。不建议去,因为j = next[j],j=1时就是死循环,而j需要从1变成0。
    return next;
}
console.log(get_next(str1)); // [undefined, 0, 1, 1, 2, 2, 3, 1, 2 ]
 
const S = "ababcabcacbab";
const T = "abcac";
 
function Index_KMP(S, T, _pos = 0) {
    let j = 0;
    let i = _pos;
    const next = get_next(T); // 获取next的计算值
    console.log("next:", next);
    while(i < S.length && j < T.length) { // 匹配没结束
        if (S[i] == T[j]) {
            // j = 0意味着也要重新开始匹配
            i++;
            j++;
        } else if(j == 0) {
            i++;
        } else {
            j = next[j];
        }
 
    }
    if(j == T.length) {
        return i - j + 1;
    } else return -1;
}
console.log(Index_KMP(S,T));

需要注意的是本文没有像书本那样将if条件合并就是为了方便理解,因为它们的含义是不一样的。 

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值