DS博客作业03--树
0.PTA得分截图
1.本周学习总结
1.1 总结树及串内容
1.1.1串
- 串的定义:零个或多个字符组成的有限序列
- 串的存储结构:
- 用一组地址连续的存储单元存储串值的字符序列,称为顺序串。可用一个数组来表示。
结构体:
#define MaxSize 100 //MaxSize常量表示字符串最大长度。
typedef struct
{ char data[MaxSize];//data域存储字符串,
int length;//length域存储字符串长度,
} SqString;
堆分配存储:以一组地址连续的存储单元存放串值字符序列,但它们的存储空间是在程序执行过程中动态分配而得的。
结构体:
typedef struct {
char *ch; // 若是非空串,则按串长分配存储区,
// 否则ch为NULL
int length; // 串长度
} HString;
- 串的链式存储:链串中的一个结点可以存储多个字符。通常将链串中每个结点所存储的字符个数称为结点大小。
结构体:
typedef struct snode
{ char data;
struct snode *next;
} LiString;
- C++的string类:
-
初始化:
首先,为了在程序中使用string类型,必须包含头文件。注意这里不是string.h,string.h是C字符串头文件。
string类是一个模板类,位于名字空间std中,通常为方便使用还需要增加:using namespace std;声明一个字符串变量很简单:string str; -
比较操作:
用 ==、>、<、>=、<=、和!=比较字符串,可以用+或者+=操作符连接两个字符串,并且可以用[]获取特定的字符。
-
string属性函数:
str.capacity(); //返回当前容量(即string中不必增加内存即可存放的元素个数)
str.max_size()const; //返回string对象中可存放的最大字符串的长度
str.size()const; //返回当前字符串的大小
str.length(); //返回当前字符串的长度(貌似和size没有区别)
str.empty(); //当前字符串是否为空
str.resize(int n,char c);或者str.resize(int n) //把字符串当前大小置为len,多去少补,多出的字符c填充不足的部分 -
string查找:
size_type find( const basic_string &str, size_type index ); //返回str在字符串中第一次出现的位置(从index开始查找),如果没找到则返回string::npos
size_type find( const char *str, size_type index ); // 同上
size_type find( const char *str, size_type index, size_type length ); //返回str在字符串中第一次出现的位置(从index开始查找,长度为length),如果没找到就返回string::npos
size_type find( char ch, size_type index ); // 返回字符ch在字符串中第一次出现的位置(从index开始查找),如果没找到就返回string::npos
(注意:查找字符串a是否包含子串b,不是用 strA.find(strB) > 0 而是 strA.find(strB) != string:npos) -
其他常用函数:
(注:参考自:博主Do Better)
- 串的BF\KMP算法
- BF算法
int index(SqString s,SqString t)
{ int i=0, j=0;
while (i<s.length && j<t.length)
{ if (s.data[i]==t.data[j]) //继续匹配下一个字符
{ i++; //主串和子串依次匹配下一个字符
j++;
}
else //主串、子串指针回溯重新开始下一次匹配
{ i=i-j+1; //主串从下一个位置开始匹配
j=0; //子串从头开始匹配
}
}
if (j>=t.length)
return(i-t.length); //返回匹配的第一个字符的下标
else
return(-1); //模式匹配不成功
}
- KMP算法
KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的
a. 得到next数组
void GetNext(SqString t,int next[])
{ int j,k;
j=0;k=-1;next[0]=-1;
while (j<t.length-1)
{ if (k==-1 || t.data[j]==t.data[k])
{ j++;k++;
next[j]=k;
}
else k=next[k];
}
}
这句代码(else k=next[k];)不好理解,(博主DK丶S讲的极好)
此外,要理解next数组的定义,next[j]数组表示当不匹配时j指针的下一位置,又指的是最大真子串的大小
b. next加强版
void GetNextval(SqString t,int nextval[])
{ int j=0,k=-1;
nextval[0]=-1;
while (j<t.length)
{ if (k==-1 || t.data[j]==t.data[k])
{ j++;k++;
if (t.data[j]!=t.data[k])
nextval[j]=k;
else
nextval[j]=nextval[k];//当两个字符相同时就跳过
}
else
k=nextval[k];
}
}
c. KMP算法
int KMPIndex1(SqString s,SqString t)
{ int nextval[MaxSize],i=0,j=0;
GetNextval(t,nextval);
while (i<s.length && j<t.length)
{ if (j==-1 || s.data[i]==t.data[j])
{ i++;
j++;
}
else
j=nextval[j];
}
if (j>=t.length)
return(i-t.length);
else
return(-1);
}
(具体思路参考:详细解析)
1.1.2树
二叉树
-
基本术语:
根:即根节点(没有前驱) 叶子:即终端节点(即没有后继) 森林:指m棵不相交的树的集合(将根删去后的子树构成) 双亲:上层的那个结点(直接前驱) 孩子:下层结点的子树的根(直接后继) 兄弟:同一双亲下的同层结点(孩子之间互称兄弟) 祖先:即从根到该结点所经分支的所有结点
子孙:即该结点下层子树中的任一结点 结点:即树的数据元素 结点的度:当前结点的分支数 结点的层次:从根节点(当成第一层)到该节点的层数 -
定义:是n(n>=0)个结点的有限集合,它或为空树(n=0),或由一个根结点和至多两棵称为根的左子树和右子树的互不相交的二叉树组成。
-
满二叉树
a. 特点:(1)如果所有分支结点都有双分结点; (2)并且叶结点都集中在二叉树的最下一层。
b. 高度为h的二叉树有2的h次方-1个结点 -
完全二叉树
a. 特点:(1)没有单独右分支结点 (2)完全二叉树虽然前n-1层是满的,但最底层却允许在右边缺少连续若干个结点。 (3)完全二叉树实际上是对应的满二叉树删除叶结点层最右边若干个结点得到的。
b. (1)度为1的结点由总结点数n的奇偶性决定,n为奇数则n1=0,n为偶数则n1=1。 (2)度为1节点,只可能有一个,且该结点只有左孩子无右孩子。 (3)若结点编号为i则双亲结点编号为i/2取下界 (4)编号为i的结点有左孩子结点,则左孩子结点的编号为2i;若编号为i的结点有右孩子结点,则右孩子结点的编号为2i+1。 (5)若i≤(n/2取下界),则编号为i的结点为分支结点,否则为叶结点。 -
二叉树性质
a. 非空二叉树上叶节点数等于双分支节点数加1。
b. 具有n个结点的完全二叉树的深度必为(log2n取下界) + 1。
c. 在二叉树的第 i 层上至多有 个结点(i≥1)。 -
二叉树存储结构
a. 顺序存储结构
b. 链式存储结构
- 结构体:
typedef struct node
{ ElemType data;
struct node *lchild, *rchild;
} BTNode;
typedef BTNode *BTree;
c. 二叉树遍历
- 递归法(先序,中序,后序)
void PreOrder(BTree bt)
{ if (bt!=NULL)
{ printf("%c ",bt->data);//先序
PreOrder(bt->lchild);
//printf("%c ",bt->data); 中序
PreOrder(bt->rchild);
//printf("%c ",bt->data); 后序
}
}
- 层次遍历
void LevelOrder(BTree bt)
{
queue<BTree>q;
BTree p;
int i = 0;
if (bt != NULL) q.push(bt);
while (!q.empty())
{
p = q.front();
q.pop();
if (p->lchild)
{
q.push(p->lchild);
}
if (p->rchild)
{
q.push(p->rchild);
}
if (i != 0) cout << " ";
cout << p->data; i++;
}
}
d. 二叉树的建立
- 递归法
BTree CreatTree(string str, int &i)
{
BTree bt;
int len = 0;
len = str.size();
if (i > len - 1) return NULL;
if (str[i] == '#') return NULL;
bt = new BTNode;
bt->data = str[i];
bt->lchild = CreatTree(str, ++i);
bt->rchild = CreatTree(str, ++i);
return bt;
}
BTree CreateBTree()
{
BTree bt = NULL;
char ch;
scanf("%c", &ch);
if (ch != '#')
{
bt = new BTNode;
bt->data = ch;
bt->lChild = CreateBTree();
bt->rChild = CreateBTree();
}
return bt;
}
- 用前序和中序创建二叉树
BTree CreateBTreePI(char ps[],char is[],int n )
{
if(n<=0)
{
return NULL;
}
BtNode* p=(BtNode*)malloc(sizeof(BtNode));
int i=0;
int m;
while(i<n)
{
if(ps[0]==is[i])
{
m=i;
break;
}
++i;
}
if(i>=n)
{
return NULL;
}
p->data=ps[0];
p->leftchild=CreateTreePI(ps+1,is,m);
p->rightchild=CreateTreePI(ps+m+1,is+m+1,n-m-1);
return p;
}
- 后序和中序构造二叉树
BTree CreateBTreeIL(char *post,char *in,int n)
{ BTNode *s; char *p; int k;
if (n<=0) return NULL;
s=new BTNode;//创建节点
s->data=*(post+n-1); //构造根节点。
for (p=in;p<in+n;p++)//在中序中找为*ppos的位置k
if (*p==*(post+n-1))
break;
k=p-in;
s->lchild=CreateBT2(post,in,k); //构造左子树
s->rchild=CreateBT2(post+k,p+1,n-k-1);//构造右子树
return s;
}
- 层次法
void CreateBTreeLevel(BiTree &rt,int n)///层序遍历建树
{
char root[2];
scanf("%s",root);
rt=new BiTreeNode;
rt->data=root[0];
for(int i=2; i<=n; i++)
{
BiTreeNode* T=rt;
char tmp[2];
int num=0,a[16];
scanf("%s",tmp);
int ii=i;
while(ii)
{
a[num++]=ii%2;
ii/=2;
}
BiTreeNode *node=new BiTreeNode;
node->data=tmp[0];
node->rchild=NULL;
node->lchild=NULL;
for(int j=num-2; j>0; j--)
if(a[j])T=T->rchild;
else T=T->lchild;
if(a[0])T->rchild=node;
else T->lchild=node;
}
}
树
a. 树的存储结构
- 双亲存储结构
typedef struct
{ ElemType data; //结点的值
int parent; //指向双亲的位置
} PTree[MaxSize];
- 孩子链存储结构
typedef struct node
{ ElemType data;//结点的值
struct node *sons[MaxSons];//指向孩子结点
} TSonNode;
3. 兄弟链存储结构
typedef struct tnode
{ ElemType data; //结点的值
struct tnode *son; //指向兄弟
struct tnode *brother; 2//指向孩子结点
} TSBNode;
小总结:当要查找某结点的祖先结点时采用双亲存储结构,当查找某结点的所有兄弟时采用孩子或孩子兄弟链存储结构
b. 树的遍历
- 先根遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。
- 后根遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点。
- 层次遍历:若树不空,则自上而下、自左至右访问树中每个结点。
c. 森林,树,二叉树之间的转化
- 森林->二叉树
- 树->二叉树
- 二叉树->森林
线索二叉树
a. 有n个结点的二叉树共有2n个指针域,有效指针域有n-1个,空指针域有n+1个,利用这些空链域,指向该线性序列中的“前驱”和“后继”的指针,称作线索。
b. 规则:(1)若结点有左子树,则lchild指向其左孩子;否则,lchild指向其直接前驱(即线索);(2)若结点有右子树,则rchild指向其右孩子;否则,rchild指向其直接后继(即线索) 。
c. 结构体
typedef struct node
{ ElemType data; //结点数据域
int ltag,rtag; //增加的线索标记
struct node *lchild; //左孩子或线索指针
struct node *rchild; //右孩子或线索指针
} TBTNode;
哈夫曼树
a.定义:具有最小带权路径的二叉树叫做哈夫曼树又称最优树(带权路径:从根节点到各个叶子节点的路径长度与相应节点权值的乘积的和,叫做二叉树的带权路径长度。)
b.哈夫曼树的创建原则:权值大的结点靠近根,权值小的远离根
c.特点:
1.由其创建过程可知,哈夫曼树没有单分支结点,并且WPL=各分支结点的权重和。
2.当哈夫曼树有N个叶子结点时,哈夫曼树的总结点数有2N-1个
d.创建:
1.结构体:
typedef struct
{ char data;//节点值
float weight;//权重
int parent;//双亲节点,用来记录双亲结点在数组中的位置
int lchild;//左孩子节点
int rchild;//右孩子节点
} HTNode;
2.创建思路(1):
(1)首先创建一个大小为2N-1的数组ht[],初始化所有结点的双亲、左右孩子域为-1,前N个位置放叶子结点
(2)从i=N开始构造非叶子结点,在前i-1个位置中找出最小左右结点ht[rnode]和ht[lnode],使这两个结点的双亲域指向i,并以这两个结点的权重和作为ht[i]的权重。
(3)代码:
void creatHuffmanTree(HTNode ht[],int n){
int i,j;
int lchild,rchild;
double minL,minR;
for(i=0;i<2*n-1;i++){
ht[i].parent = ht[i].lchild = ht[i].rchild = -1;
}
for(i=n;i<2*n-1;i++){
minL = minR = MAXNUMBER;
lchild = rchild = -1;
for(j=0;j<i;j++){
if(ht[j].parent == -1){
if(ht[j].weight < minL){
minR = minL;
minL = ht[j].weight;
rchild = lchild;
lchild = j;
}else if(ht[j].weight < minR){
minR = ht[j].weight;
rchild = j;
}
}
}
ht[lchild].parent = ht[rchild].parent = i;
ht[i].weight = minL + minR;
ht[i].lchild = lchild;
ht[i].rchild = rchild;
}
}
3.创建思路(2)(利用queue,但是没有具体的树结构):
(1)这里用到优先队列这个神器,优先队列具有队列的所有特性,包括队列的基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的,但是这东西就只能求wpl。
priority_queue<Type, Container, Functional>//分别指数据类型,容器类型,比较的方式(升序、降序最常见)
(2)一直取优先队列的前两个出队,相加,和再入队,最后的和就是wpl了。
(3)代码:
priority_queue<int, vector<int>, greater<int> > prio_que;//数据类型,容器类型,比较的方式(升序)
int main()
{
int n, sum = 0;
cin >> n;
for (int i = 0; i < n; i++)
{
int a;
cin >> a;
prio_que.push(a);
}
for (int i = 0; i < n - 1; i++)
{
int a = prio_que.top();
prio_que.pop();
int b = prio_que.top();
prio_que.pop();
sum += (a + b);
prio_que.push(a + b);
}
cout << sum << endl;
return 0;
}
d.哈夫曼编码:
(1)核心是使总编码长度尽可能的短,并且使任一字符的编码都不是另一个字符的编码的前缀。
(2)执行的是从叶子结点到根节点的遍历
(3)结构体和代码:
typedef struct
{
char cd[N];//存放哈夫曼码
int start;
} HCode;
void createHuffmanCode(HTNode ht[],HCode hc[],int n)
{
int i,f,c;
HCode father;
for(i=0;i<n;i++)
{
hc[i].start = n;
c = i;
while((f=ht[c].parent) != -1) //当到根结点时结束
{
if(ht[f].lchild == c){ //左孩子结点
hc[i].code[hc[i].start--] = '0';
}else{ //右孩子结点
hc[i].code[hc[i].start--] = '1';
}
c = f;
}
hc[i].start++;
}
}
并查集
a.用途:查找一个元素所属的集合及合并2个元素各自专属的集合等运算。
b.创建并查集树:
1.结构体:
typedef struct node
{ int data; //结点对应人的编号
int rank; //结点秩:子树的高度,合并用
int parent; //结点对应双亲下标
} UFSTree;
2.初始化:
void MAKE_SET(UFSTree t[],int n) //初始化并查集树
{ int i;
for (i=1;i<=n;i++)
{ t[i].data=i; //数据为该人的编号
t[i].rank=0; //秩初始化为0
t[i].parent=i; //双亲初始化指向自已!!
}
}
3.查找元素所属集合:
int FIND_SET(UFSTree t[],int x) //在x所在子树中查找集合编号
{ if (x!=t[x].parent) //双亲不是自已
return(FIND_SET(t,t[x].parent)); //递归在双亲中找x
else
return(x); //双亲是自已,返回x
}
4.查找两个元素各自所属的集合的合并
void UNION(UFSTree t[],int x,int y) //将x和y所在的子树合并
{
x=FIND_SET(t,x); //查找x所在分离集合树的编号
y=FIND_SET(t,y); //查找y所在分离集合树的编号
if (t[x].rank>t[y].rank) //y结点的秩小于x结点的秩
t[y].parent=x; //将y连到x结点上,x作为y的双亲结点
else //y结点的秩大于等于x结点的秩
{
t[x].parent=y; //将x连到y结点上,y作为x的双亲结点
if (t[x].rank==t[y].rank) //x和y结点的秩相同
t[y].rank++; //y结点的秩增1
}
}
1.2.谈谈你对树的认识及学习体会。
树结构特别是二叉树是一种特别重要的数据结构,是线性结构到图结构的过渡,学好树非常有必要,树结构我感觉比较难的部分是搞清楚各种建树和遍历的方法,还有就是递归思想的应用,以及队列和栈结构在树遍历中的应用,想要熟练掌握树结构还是要着手写代码,多用用才能理解原理才能熟练,还有就是画图真的能够帮助理解,遇到不会的可以借助画图来理解。
2.阅读代码(0--5分)
2.1 二叉树中的最大路径和
a. 题目:
b. 代码:
int maxPathSum(TreeNode* root, int &val)
{
if (root == nullptr) return 0;
int left = maxPathSum(root->left, val);
int right = maxPathSum(root->right, val);
int lmr = root->val + max(0, left) + max(0, right);
int ret = root->val + max(0, max(left, right));
val = max(val, max(lmr, ret));
return ret;
}
int maxPathSum(TreeNode* root)
{
int val = INT_MIN;
maxPathSum(root, val);
return val;
}
2.1.1 该题的设计思路
2.1.2 该题的伪代码
if(树为空)then[返回0]
计算左边分支最大值,负数舍弃
计算右边分支最大值,负数舍弃
分成2,3和1讨论
L->root->R作为路径与历史最大值比较
返回2,3情况的最大值
2.1.3 运行结果
2.1.4分析该题目解题优势及难点。
解题思路清晰,分情况讨论
2.2 二叉树剪枝
a.题目:
来源
b.代码:
TreeNode* pruneTree(TreeNode* root) {
if(!root) return root;
stack<TreeNode*> S;
TreeNode *node = root;
S.push(root);
while(!S.empty()){
if(S.top()->left != node && S.top()->right != node){
getLeaf(S);
}
node = S.top();
S.pop();
//遍历节点,判断是否剪掉子节点
if(node->left){
if(0 == node->left->val && NULL == node->left->left && NULL == node->left->right){
node->left = NULL;
}
}
if(node->right){
if(0 == node->right->val && NULL == node->right->left && NULL == node->right->right) {
node->right = NULL;
}
}
}
return root;
}
void getLeaf(stack<TreeNode*> &S){
while(TreeNode *x = S.top()){
if(x->left){
if(x->right){
S.push(x->right);
}
S.push(x->left);
}
else{
S.push(x->right);
}
}
S.pop();
}
2.2.1 该题设计思路
可剪的枝要满足两个条件:(1)必须为叶子结点 (2)结点值要为0
因此需要用后序遍历从叶子到根遍历,要从下到上还要先找到左或右子树最深的叶子结点