DS博客作业03--树
0.PTA得分截图。
1.本周学习总结。
1.1 总结树及串内容。
串的BF算法。
BF算法简单且易于理解,但是效率不高,每一次匹配失败时,主串都需要回溯。
假设主串长度为m,模式串长度为n,则BF算法的时间复杂度是O(n*m)。
/*BF算法*/
int BF(string s,string t)
{
int i=0,j=0;
while(i<s.size() && j<t.size()) //两个串都没扫描完时循环
{
if(s[i]==t[i]) //当前两个字符相同,比较后续两个字符
{
i++;
j++;
}
else //当前两个字符不相同,主串回退,子串从头开始匹配
{
i=i-j+1;
j=0;
}
}
if(j>=t.size()) //j越界,说明t是s的子串
{
return i-t.size();
}
return -1;
}
串的KMP算法。
kmp算法主要是减少字符串查找过程中的回退,尽可能减少不用的操作,算法复杂度时间是O(n+m)。
/*KMP算法*/
int KMP(string s, string t)
{
int nextval[100], i = 0, j = 0;
GetNextval(t, nextval); //先求出nextval数组
while(i < (int)s.size()&& j < (int)t.size()) //两个串都没扫描完时循环
{
if (j == -1 || s[i] == t[j]) //两个字符相等或j=-1,比较后续两个字符
{
j++;
i++;
}
else //两个字符不相等,获取j
{
j = nextval[j];
}
}
if (j >= (int)t.size()) //j可能是负数,s.size()返回的是无符号数,需要转成int类型再比较。
{
return i - (int)t.size();
}
else
{
return -1;
}
}
/*nextval数组*/
void GetNextval(string t,int nextval[])
{
int j = 0, k = -1;
nextval[0] = -1;
while (j < t.size())
{
if (k == -1 || t[j] == t[k])
{
j++;
k++;
if (t[j] != t[k])
{
nextval[j] = k;
}
else
{
nextval[j] = nextval[k];
}
}
else
{
k = nextval[k];
}
}
}
二叉树的定义、性质、存储结构、建法、遍历及应用。
二叉树定义
二叉树是一种树形结构,特点是与每个节点关联的子节点至多有两个(可为0,1,2),每个节点的子节点有关联位置关系。
满二叉树:树中每个分支节点(非叶节点)都有两棵非空子树。
完全二叉树:除最下两层外,其余节点度数都是2,如果最下面的节点不满,则所有空位都在右边,左边没有空位。
二叉树性质
性质 | 内容 |
---|---|
性质1 | 在二叉树的第i层上至多有2^(k-1)个节点(i >= 1) |
性质2 | 深度为k的二叉树至多有2^k - 1个节点(k >= 1) |
性质3 | 对任何一颗二叉树T,如果其终端节点数为n0,度为2的节点数为n2,则n0 = n2 + 1 |
性质4 | 具有n个节点的完全二叉树的深度为[log2n] + 1 ([]表示向下取整) |
性质5 | 如果对一颗有n个节点的完全二叉树(其深度为[long2n] + 1)的节点按层序编号(从第1层到第[log2n] + 1层,每层从左到右),对任一节点i(1 <= i <= n)有 1.如果i = 1,则节点i是二叉树的根,无双亲;如果i > 1,则双亲是节点[i / 2] 2.如果2i > n,则节点i无左孩子(节点i为叶子结点);否则其左孩子是节点2i 3.如果2i + 1 > n,则节点i无右孩子;否则其右孩子是节点2i + 1 |
二叉树顺序存储结构
完全二叉树和满二叉树的顺序存储结构可以用一维数组按从上到下、从左到右的顺序存储数中的所有结点值,通过数组元素的下标关系反映完全二叉树或满二叉树结点间的逻辑关系。
对于下标为i的树结点,下标为i/2的结点是其双亲,下标为2i的结点是其左孩子,下标为2i+1的结点是其右孩子。
但由于采用顺序存储结构,数组的固有缺陷一直存在,使得二叉树的插入、删除等运算十分不便。
二叉树链式存储结构
二叉树的链式存储结构通常简称二叉链,对于一般的二叉树比较节省存储空间,在二叉链中访问一个结点的孩子很方便,但访问一个结点的双亲结点需要扫描所有结点。
对于链式存储结构而言,进行二叉树结点的插入、删除比较简单方便。
二叉树的建法
建树方法 | 内容 |
---|---|
方法1 | 顺序存储结构转二叉链 |
方法2 | 前序序列创建二叉树 |
方法3 | 前序和中序序列创建二叉树 |
方法4 | 后序和中序序列创建二叉树 |
方法5 | 括号法创建二叉树(使用栈) |
方法6 | 层序创建二叉树(使用队列) |
方法1:
/*顺序存储结构转二叉链*/
typedef struct BiTNode
{
char data;
struct BiTNode* lchild, * rchild; //指向左右孩子
}BTNode, * BTree;
BTree CreatTree(string s, int i) //顺序存储结构用字符串s表示
{
BTree bt;
bt = new BTNode;
int len;
len = s.size(); //字符串长度
if (i >len-1) //超过长度返回NULL
{
return NULL;
}
if ( s[i] == '#') //空结点返回NULL
{
return NULL;
}
/*根据左右孩子间i的关系递归建树*/
bt->data = s[i];
bt->lchild = CreatTree(s, 2 * i);
bt->rchild = CreatTree(s, 2 * i + 1);
return bt;
}
方法2:
/*前序序列创建二叉树*/
BTree CreatBTree(string s,int &i) //前序序列s,i为字符的下标
{
BTree bt;
if (i==s.size()||s[i] == '#') //超过s长度或为空结点返回NULL
{
return NULL;
}
bt = new BNode;
/*前序遍历建树*/
bt->data = s[i];
bt->lchild = CreatBTree(s,++i);
bt->rchild = CreatBTree(s,++i);
return bt;
}
方法3:
/*前序和中序序列创建二叉树*/
BTree CreatBTree(char* pre, char* in, int n) //前序序列用数组pre存储,中序序列用数组in存储
{
BTree bt;
int k;
char* p;
if (n <= 0) return NULL;
bt = new BTNode;
bt->data = *pre; //根节点位置
for (p = in; p < in + n; p++) //在中序序列中找到根节点位置
{
if (*p == *pre) break;
}
k = p - in;
bt->lchild = CreatBTree(pre + 1, in, k);
bt->rchild = CreatBTree(pre + k + 1, p + 1, n - k - 1);
return bt;
}
方法4:
/*后序和中序序列创建二叉树*/
BTree CreatBTree(int* post, int* in, int n) //后序序列用数组post存储,中序序列用数组in存储
{
BTree bt;
int k, * p;
if (n <= 0) return NULL;
bt = new BTNode;
bt->data = *(post + n - 1); //后序序列末尾为根节点
for (p = in; p < in + n; p++) //在中序序列中找到根节点
{
if (*p == *(post + n - 1)) break;
}
k = p - in;
bt->lchild = CreatBTree(post, in, k);
bt->rchild = CreatBTree(post + k, p + 1, n - k - 1);
return bt;
}
二叉树遍历
遍历方法 | 内容 |
---|---|
方法1 | 先序遍历 |
方法2 | 中序遍历 |
方法3 | 后序遍历 |
方法4 | 层次遍历 |
方法1:
/*先序遍历*/
void Preorder(BinTree BT)
{
if (BT==NULL) return;
/*根结点->左孩子->右孩子*/
cout>> BT->Data;
Preorder(BT->Left);
Preorder(BT->Right);
}
方法2:
/*中序遍历*/
void inorder(BinTree BT)
{
if (BT==NULL) return;
/*左孩子->根结点->右孩子*/
Preorder(BT->Left);
cout>> BT->Data;
Preorder(BT->Right);
}
方法3:
/*后序遍历*/
void postorder(BinTree BT)
{
if (BT==NULL) return;
/*左孩子->右孩子->根结点*/
Preorder(BT->Left);
Preorder(BT->Right);
cout>> BT->Data;
}
方法4:
/*层序遍历*/
void LevelOrder(BTree bt)
{
queue<BTree>q; //使用队列遍历
BTree T;
q.push(bt); //根节点入队
while (!q.empty()) //当队列不为空循环
{
T = q.front();
cout<< T->data ;
q.pop(); //出队一个结点,入队该结点的左右孩子
if (T->lchild)
{
q.push(T->lchild);
}
if (T->rchild)
{
q.push(T->rchild);
}
}
}
二叉树应用
求二叉树高度
int GetHeight(BinTree BT)
{
int h1,h2;
if (BT == NULL)
{
return 0;
}
h1 = GetHeight(BT->Left);
h2 = GetHeight(BT->Right);
if (h1 > h2)
{
return h1+1;
}
return h2+1;
}
求二叉树中结点x所在高度
int Getlevel(BTree bt,char x,int h)
{
int l=0;
if(bt==NULL) return;
if(bt->data==x) return h;
l=Getlevel(bt->lchild,x,h+1);
/*通过函数返回值判断是否结束递归*/
if(l==0) //找不到继续遍历左子树
{
l=Getlevel(bt->rchild,x,h);
}
else
return l; //找到返回
}
表达式树
将中缀表达式生成二叉树,再后序遍历二叉树得到后缀表达式求解。
void InitExpTree(BTree& T, string str)
{
stack<char>opSt;
stack<BTree>BtSt;
BTree a, b;
int i=0;
while(str[i])
{
if (In(str[i]))//运算符
{
if (opSt.empty() || Precede(opSt.top(),str[i])=='<')//运算符栈空||优先级大于栈顶
{
opSt.push(str[i++]);
}
else if (Precede(opSt.top(), str[i]) == '>')//优先级小于栈顶
{
b = BtSt.top();
BtSt.pop();
a = BtSt.top();
BtSt.pop();
CreateExpTree(T,a,b,opSt.top());//弹出一个运算符和两个操作数生成树根
BtSt.push(T);
opSt.pop();
}
else
{
opSt.pop();//左右括号匹配出栈
i++;
}
}
else//操作数生成树根
{
T = new BTNode;
T->data = str[i++];
T->lchild = NULL;
T->rchild = NULL;
BtSt.push(T);
}
}
while (!opSt.empty())//出栈剩余元素
{
b = BtSt.top();
BtSt.pop();
a = BtSt.top();
BtSt.pop();
CreateExpTree(T, a, b, opSt.top());//弹出一个运算符和两个操作数生成树根
BtSt.push(T);
opSt.pop();
}
}
double EvaluateExTree(BTree T)
{
double a, b;
if (!T->lchild && !T->rchild)//将叶结点的字符型运算数转为整型
{
return T->data - '0';
}
//后序遍历表达式树,生成后缀表达式
a = EvaluateExTree(T->lchild);
b = EvaluateExTree(T->rchild);
switch (T->data)
{
case '+':return a + b;
case '-':return a - b;
case '*':return a * b;
case '/':
if (b == 0)
{
cout << "divide 0 error!";
exit(0);//除0错误退出程序
}
return a / b;
}
}
树的定义、术语、性质、存储结构、操作、遍历及应用。
树的定义
树是n(n>=0)个结点的有限集合.当n=0时,集合为空,称为空树.在任意一颗非空树中,有且仅有一个特定的结点称为根.当n>1时,除根结点以外的其余结点可分成m(m>=0)个不相交的有限结点集合T1,T2….Tm.其中每个集合本身也是一棵树,称为根的子树.
其中:
(1)有且仅有一个称为根的结点:它没有前继结点,有0个或者多个后继结点.
(2)有若干个称为叶的结点:它们有且仅有一个前几节点,而没有后继结点.
(3)其余称为节的结点:它们有且仅有一个前继结点,至少有一个后继结点.
实际上,树表示了一组结点之间不同于线性表的前继和后继关系的数据结构.一般而言,树种任何一个结点只有一个前继(根结点除外),可以有多个后继(叶结点除外).
树的术语
(1)结点的度:一个结点拥有子树(或后继结点)的个数称为度.度是结点分支树的表示.
(2)树的度:树中所有结点的度的最大值称为树的度.
(3)子结点:一个结点的子树的根节点(或直接后继结点)称为该结点的子结点.
(4)父结点:一棵子树根结点的前继结点称为父结点.除根结点以外的任何结点有且仅有一个父结点.父结点也称双亲结点.
(5)兄弟结点:属于同一个父结点的若干子结点之间互称兄弟结点.
(6)结点的层:树的任何一个结点都处于某一层.因此,树中结点构成一个层次结构.
(7)树的深度:树的最大层次数称为树的深度,也称树的高度.
(8)有序树和无序数:若把结点的子结点都看成从左向右是有序的,则称为有序树;否则称为无序树.有序树的兄弟结点不可交换.
(9)森林:树的集合称为森林.树和森林之间有着密切的关系.删除一个树的根结点,其所有原来的子树都是树,构成森林.用一个结点连接到森林的所有树的根结点就构成树.这个结点称为新的根结点.森林的所有树是该结点的子树.
树的性质
性质 | 内容 |
---|---|
性质1 | 数中的结点数等于所有结点的度数之和加1。 |
性质2 | 度为m的树中第i层上最多有m^(i-1)个结点。(i>=1) |
性质3 | 高度为h的m次树最多有(m^h-1)/(m-1)个结点。 |
性质4 | 具有n个结点的m次树最小高度为[logm(n(m-1))]+1。([]表示向下取整) |
树的存储结构
双亲存储结构:
双亲存储结构是一种顺序存储结构,设伪指针指向双亲位置。
在这种存储结构中,求某个结点的双亲结点十分容易,但在求某个结点的孩子结点时需要遍历整个存储结构。
typedef struct
{
int data;
int parent; //存放双亲位置
}PTree[MaxSize];
孩子链存储结构:
孩子链存储结构需要按照树中所有结点最大的度设计结点的孩子结点的指针域个数。
在这种存储结构中,求某个结点的孩子结点十分方便,但在求某个结点的双亲结点比较费时,而且当树的度较大时存在较多空指针域。
typedef struct
{
int data;
struct node *sons[MaxSize]; //指向孩子结点
}TSonNode;
孩子兄弟链存储结构:
孩子兄弟链存储结构中一个指针指向左边第一个结点,一个指针指向兄弟结点。
孩子兄弟链存储结构可以方便实现树和二叉树的相互转换,缺点是查找双亲结点比较麻烦。
typedef struct tnode
{
int data;
struct tnode *hp; //指向兄弟
struct tnode *vp; //指向孩子结点
}TSBNode;
树的操作
(1)创建树
(2)判树空
(3)求树根
(4)求树高
(5)求树结点
(6)求父结点
(7)求子结点
(8)插入结点
树的遍历
方法 | 内容 |
---|---|
方法1 | 先根遍历 |
方法2 | 后根遍历 |
方法3 | 层序遍历 |
树的应用
部分数据库
文件系统
线索二叉树。
对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。
二叉树线索化
若结点的左子树为空,则该结点的左孩子指针指向其前驱结点。
若结点的右子树为空,则该结点的右孩子指针指向其后继结点。
按照遍历顺序可分为先序线索树、中序线索树、后序线索树。
哈夫曼树、并查集。
哈夫曼树
带权路径长度(WPL):设二叉树有n个叶子结点,每个叶子结点带有权值乘从根结点到每个叶子结点的长度之和为WPL。
最优二叉树或哈夫曼树: WPL最小的二叉树。
哈夫曼树的特点:
特点 | 内容 |
---|---|
特点1 | 没有度为1的结点. |
特点2 | n个叶子结点的哈夫曼树共有2n-1个结点。 |
特点3 | 哈夫曼树的任意非叶节点的左右子树交换后仍是哈夫曼树。 |
特点4 | 对同一组权值{w1 ,w2 , …… , wn},存在不同构的两棵哈夫曼树。 |
哈夫曼树的构造就是每次把权值最小的两颗二叉树合并:
哈弗曼编码:
前缀码:任何字符的编码都不是另一字符编码的前缀可以无二义地解码。
构造哈弗曼树实现前缀编码,左分支用0表示,右分支用1表示。
并查集
在并查集中,n和不同的元素被分为若干组,每组是一个集合,称为分离集合。
结构体定义:
typedef struct node
{
int data;
int rank; //结点的秩
int parent; //结点双亲下标
}UFSTree;
并查集树的初始化:
void MAKE_SET(UFSTree t[], int n)
{
int i;
for (i = 0; i <= n; i++)
{
t[i].data = i;
t[i].rank = 0;
t[i].parent = i; //双亲指向自己
}
}
查找一个元素所属的集合:
int FIND_SET(UFSTree t[], int x)
{
if (x != t[x].parent) //双亲不是自己
return(FIND_SET(t, t[x].parent)); //递归在双亲中找x
else
return x; //双亲是自己,返回
}
两个各自所属集合合并:
void UNION(UFSTree t[], int x, int y)
{
x = FIND_SET(t, x); //查找x所在集合编号
y = FIND_SET(t, y); //查找y所在集合编号
if (t[x].rank > t[y].rank)
t[y].parent = x; //将y连到x结点上
else
{y
t[x].parent = y; //将x连到y结点上
if (t[x].rank == t[y].rank)
t[y].rank++;
}
}
1.2.谈谈你对树的认识及学习体会。
- 树属于非线性结构,在数中一个结点可以与多个结点对应,可以表示层级结构的数据。
- 编程实现树的过程中,通常出错后无法直观的看到错误的地方,调试起来更加困难。
- 树这一章的知识点较多,需要经常结合实践编程加强记忆,否则容易遗忘。
- 关于树的问题大多比线性结构的问题更加复杂,编程难度更大。
- 树的操作大量使用递归,对递归的理解也对树的理解有很大帮助。
- 这部分内容感觉上挺难的,相比堆、栈结构树更加抽象隐晦。
2.阅读代码
2.1 题目及解题代码
2.1.1 该题的设计思路
递归后续遍历二叉树,当遍历置某个节点时,更新该节点的值。使其变为“抢劫以该节点为根的子树所能得到的最大收益”。由递归定义,最后返回根节点的值即可。
从两个选项其中之一得到打劫的最大收益:
1.抢劫根节点,并抢劫子节点的子节点。
2.不抢劫根节点,并抢劫子节点。
时间复杂度O(n)
空间复杂度O(1)
2.1.2 该题的伪代码
定义l,r,ll,lr,rl,rr; //分别代表打劫左、右、左左、左右、右左、右右
if(左子树不空) 递归遍历左子树;
if(右子树不空) 递归遍历右子树;
if(左子树不空) 分别计算打劫左、左左、左右的钱;
if(右子树不空) 分别计算打劫右、右左、右右的钱;
/*计算两种打劫方法的收益并记录下收益高的一项*/
root.val=max{抢劫根节点并抢劫子节点的子节点的收益,不抢劫根节点并抢劫子节点的收益};
return root.val;
2.1.3 运行结果
2.1.4分析该题目解题优势及难点。
- 难点是遇到结点该不该抢,每个结点都有抢和不抢两种情况。判断分析出抢劫根节点并抢劫子节点的子节点、不抢劫根节点并抢劫子节点两种情况的收益。
- 优势是利用到递归、动态规划把每个所有情况全部计算出并选择收益高的情况。
2.2 题目及解题代码
2.2.1 该题的设计思路
从上到下递归,比较每个节点左右子树的高度。在一棵树中,只有每个节点左右子树高度差不大于 1 时,该树才是平衡的。
因此可以比较每个节点左右两棵子树的高度差,然后向上递归。
时间复杂度O(nlogn)
空间复杂度O(n)
2.2.2 该题的伪代码
递归获取树的高度height;
if(空树) return true;
if(左右子树高度之差绝对值大于1)
return false;
else
return isBalanced(root->left) && isBalanced(root->right); //左右子树递归继续判断高度是否满足条件
2.2.3 运行结果
2.2.4分析该题目解题优势及难点。
- 难点:对于每一棵树,每棵子树也是一个子问题,按照什么顺序处理这些子问题。
- 优势:自顶向下的递归,将每个子问题一一解决。
2.3 题目及解题代码
2.3.1 该题的设计思路
采用后序遍历的方式遍历二叉树,当遍历到根节点后,对根节点的左右子树做一些调整
将右节点挂到左节点的最右边
再将整个左子树挂到根节点的右边,这样就可以将整棵树变成链表结构了。
2.3.2 该题的伪代码
if(空树) return;
递归左子树;
递归右子树;
if(左子树不空)
{
找到左子树最右边;
右子树挂到左子树的最右边;
将整个左子树挂到根节点的右边;
左子树置为空;
}
时间复杂度:O(n)
空间复杂度:O(h),h为树的高度
2.3.3 运行结果
2.3.4分析该题目解题优势及难点。
- 难点:该题解法有许多种,该解法没有借助数组或栈等辅助,在后序遍历到根结点时,直接通过指针的指向改变结点之间的关系,从而形成链表。
- 优势:在解法上比较巧妙,在空间复杂度上有一定的优势。
2.4 题目及解题代码
2.4.1 该题的设计思路
从跟结点开始向下遍历,如果当前结点到达叶子结点下的空结点时,返回空;如果当前结点为 p 或 q 时,返回当前结点;
递归时,如果在左子树中找到了 p 或 q,left 会等于 p 或 q,同理,right 也是一样;
然后进行判断:如果 left 为 right 都不为空,则为情况 1;如果 left 和 right 中只有一个不为空,说明这两个结点在子树中,则根节点到达子树再进行寻找。
时间复杂度:O(n)
空间复杂度:O(h),h为树的高度
2.4.2 该题的伪代码
if(结点为空||结点为p||结点为q) 返回该结点值;
递归遍历左子树找p或q;
递归遍历右子树找p或q;
if(左右子树找到&&右子树找到)
return root;
if(左子树为空)
递归寻找右子树;
else
递归寻找左子树;
2.4.3 运行结果
2.4.4分析该题目解题优势及难点。
- 难点:本题难在递归函数的设计,在递归调用时,需注意由于p,q的位置不同而产生的不同情况。
- 优势:递归函数设计巧妙,在递归调用时通过递归函数的返回值判断p,q的位置,再根据返回值确定是否递归左子树或右子树。