前言
应广大支持者的要求,随我自身学习之余,肝数据结构,开车只是暂时的,飙车才是认真的,数据结构开始了,本文用c++编写,如果有不足的地方欢迎大家补充
一、数据结构
通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率,这也是一个优秀的程序员必须掌握的一项基本功,无论你学哪个语言,又提到了语言,这里在推销一波[如果你还在纠结到底哪门语言作为主语言的话不妨来看看(入门时刻)
](https://blog.csdn.net/weixin_45920495/article/details/119982904?spm=1001.2014.3001.5501)
本文我主要讲解数据结构中的
- 线性表
- 栈与队列
- 串
- 树
- 图
这也就包含了所有的数据结构了,那废话不多说,我们开始吧!
线性表
首先它是线性的,就像线一样,小时候都玩过牵着手并排走的游戏,话说我都几年没有牵过女孩手了,当时我们可以知道自己前面的人和后面的人是谁,像有线一样连在了一起
官方定义:零个或多个数据元素的有限序列
序列,有限是我们应该着重注意的地方,还是小朋友牵手,他是一个有限的序列,其中小朋友视为数据元素,,例子有很多:月份,星座,直系关系等等都是线性表
下面在拿我自己举一个例子,
码神—>18—>155*0111—>西安
顺序存储
这一通操作下来相信大家应该对线性表有一个基础的了解了吧,接着我们回到上述的拉手问题中,话说年轻码手当时喜欢一个女孩,我们姑且称她为小苗吧,我就希望自己可以拉上小苗的手,首先我要找到她,然后重新站到小苗的身边,这时线性表就发生了重排,但是啊,小苗不喜欢我,直接就出列了,所以又出现了删除的操作,上面我们可以看出来,这个线性表的基操就有查找,插入,删除
了,秉承学着就练这的原则,我现在来帮大家实现一下
#include <iostream>
#include <cstdio>
#include <string>
#include <string.h>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 10005;
const int mod = 7654321;
typedef int ElementType;
typedef struct Node
{
ElementType data[maxn];//元素
int last;//最后一个元素的下标
}*List;
/*typedef是类型定义的意思。typedef struct 是为了使用这个结构体方便。
具体区别在于:
若struct node{}这样来定义结构体的话。在申请node 的变量时,需要这样写,struct node n;
若用typedef,可以这样写,typedef struct node {}NODE; 。在申请变量时就可以这样写,NODE n;
区别就在于使用时,是否可以省去struct这个关键字。*/
//初始化一个空表
List CreatList()
{
List L;
L = (List)new int();
//开辟一块新空间
L->last = -1;
return L;
}
//查找
ElementType Search(List L, ElementType x)
{
int i = 0;
while (i <= L->last)
{
if (L->data[i] == x)
return i;
i++;
}
return -1;
}
//插入
bool Insert(List L, int position, ElementType x)
{
if (L->last == maxn - 1)
{
cout << "表已满" << endl;
return false;
}
if (position<0 || position>L->last + 1)
{
cout << "位置不合法" << endl;
return false;
}
for (int i = L->last; i >= position; i--)
{
L->data[i + 1] = L->data[i];
}
L->data[position] = x;
L->last++;
return true;
}
//删除
bool Delet(List L, int position)
{
if (position<0 || position>L->last)
{
cout << "位置不合法" << endl;
return false;
}
for (int i = position + 1; i <= L->last; i++)
{
L->data[i - 1] = L->data[i];
}
L->last--;
return true;
}
int main()
{
List L = CreatList();
Insert(L, 0, 1);
Insert(L, 1, 1);
Insert(L, 2, 2);
cout << L->data[0] << endl;
Delet(L, 1);
cout << L->data[1] << endl;
cout << Search(L, 2) << endl;
return 0;
}
我们来谈一下优缺点吧,实际上也很明显,比如我要找到小苗,一眼就能看到,但是当我要插入,或者小苗要出来时,变化的就是整个队伍了,还有不太明显的地方:当线性表长度不确定时,难以确定储存空间的大学,还可能造成存储空间的“碎片”,用new。
链式
上面我们说到顺序存储最大的缺点就是插入和删除要动所有的元素,为了解决这个问题我们引出了链式存储
总体思路就是让相邻的元素之间留够空位置,可能实现有点困难,但是 换个思路就简单多了,也就是不要考虑相邻位置了,哪里有空位置就插到哪里,而只是让每个元素知道她下一个元素的位置,这样做的话,我们可以在第一个元素时,就知道第二个元素的位置,从而再找到第三个元素的位置,有点像单线索的侦探游戏,下面我们用代码来看一下具体的实现步骤吧。
#include <iostream>
#include <cstdio>
#include <string>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
typedef long long ll;
#define maxn 10005
#define mod 7654321
typedef int ElementType;
typedef struct Node
{
ElementType data;
Node *next;
}*List;
//求表长
int Length(List L)
{
List P = L;
int num = 0;
while (P)
{
P = P->next;
num++;
}
return num;
}
//查找
/*读取,较顺序存储比较麻烦
1.声明一个指针p指向链表的第一个节点,从1开始
2.当j<i时,就遍历链表,让p的指针向后移动,j++
3.若链表末尾为空,则说明第i个节点不存在
4.查找成功,返回p的数据*/
List Search(List L, ElementType x)
{
List P = L;
while (L)
{
if (L->data == x)
return P;
L = L->next;
}
return NULL;
}
//插入
/*
1.声明一指针p指向链表头,初始化从1开始
2.p向后移动
3.查找成功,插入
4.插入标准语句
s->next=p->next;
p->next=s
*/
bool Insert(List L, ElementType x, List P)
{
List tem, Pre;
/* 查找P的前一个结点 */
while (L)
{
if (L->next == P)
{
Pre = L;
break;
}
L = L->next;
}
if (Pre == NULL)//不成功
return false;
else
{
tem = (List)malloc(sizeof(Node));
tem->data = x;
tem->next = P->next;
Pre->next = tem;
return true;
}
}
//删除
/*
类比于插入
删除标准语句p->next=q->next
q中数据赋值给e
然后释放q
*/
bool Delete(List L, List P)
{
List Pre, tem;
while (L)
{
if (L->next == P)
Pre = L;
L = L->next;
}
if (Pre == NULL)
return false;
else
{
Pre->next = P->next;
return true;
}
}
int main()
{
return 0;
}
小结
若线性表需要频繁的查找,很少进行插入和删除操作时,应该选用顺序储存结构。
若线性表需要频繁的插入和删除操作时,很少查找时,应该选用链表结构。
why?
从时间性能来考虑的话,查找:
顺序结构O(1),单链表(N)
插入删除
顺序:平均移动一半的距离O(N)
单链表在找出位置的指针后,插入和删除的时间复杂度仅为O(1)
从空间复杂度来看的话:
顺序需要预分配存储空间,,but数据是不确定的,分大浪费,分小上溢。
单链表不需要分配储存空间,只要有就可以分配,元素个数也不受限制
So,当数据大小不确定的时候,最好使用单链表,但是像一年12月,一周7天这种还是用顺序存储比较效率高一点。
栈和队列
栈:仅在表尾进行插入和删除操作的线性表
队列:只允许在一端进行插入,而在另一端进行删除的线性表
栈
如果说栈最大特征应该就是:先进后出了,像以前的沙漠之鹰的弹夹一样,先放入的子弹,后面才能打出
栈的应用在软件中也比较普遍,像浏览器中的后退一样单击后可以按访问顺序的逆序加载浏览过的网页
还有许多的文本编辑软件的“ctrl+z”的撤销功能,也是通过栈来实现的
了解了什么是栈之后,我们来说栈的几个概念
- 栈顶:允许插入和删除的一端
- 栈底:栈顶的另一端
- 空栈:不含任何元素的
- 还可以说是栈是限定仅在表尾(栈顶)进行插入和删除操作的线性表
- 插入就叫进栈
- 删除称为出栈
就像沙漠之鹰的弹夹一样,进栈为子弹弹入弹夹,出栈为子弹弹出弹夹
看例题吧,尝试一种新的写法,用算法题来解决栈的问题,因为我感觉实现和线性表的差不多
铁轨:uva514
某城市有一个火车站,铁轨铺设如图。有n节车厢从A方向驶入车站,按进站的顺序编号为1~n。你的任务是判断是否能让他们按照某种特定的顺序进入B方向的铁轨并驶出车站。例如,出栈顺序(5 4 1 2 3)是不可能的,但(5 4 3 2 1)是可能的。 为了重组车厢,你可以借助中转站C。这是一个可以停放任意多节车厢的车站,但由于末端封顶,驶入C的车厢必须按照相反的顺序驶出C。对于每节车厢,一旦从A移入C,就不能返回A了;一旦从C移入B,就不能返回C了。也就是说,在任意时刻,只有两种选择:A到C和C到B。
//首先中转站C中,车厢符合先进后出的原则,是一个栈
#include<cstdio>
#include<stack>
using namespace std;
const int maxn = 1000 + 10;
int n, target[maxn];
int main()
{
while (scanf_s("%d", &n) == 1)
{
stack<int> s;
int a = 1, b = 1;
for (int i = 1; i <= n; i++)
{
scanf_s("%d", &target[i]);
}
int ok = 1;
while (b <= n)
{
if (a == target[b])
{
a++;
b++;
}
else if (!s.empty() && s.top() == target[b])
//运用empty和top函数,包含头文件《stack》
{
s.pop();
b++;
}
else if (a <= n)
s.push(a++);
else
{
ok = 0;
break;
}
}
printf("%s\n", ok ? "yes" : "no");
}
return 0;
}
后缀表达式
提栈还是绕不开后缀表达式,我感觉我讲的也不太好,看个b站视频吧后缀表达式下面我用代码实现一下
#include <iostream>
#include <stack>
using namespace std;
//表达式求值
const int MaxSize = 100;
void trans(char exp[], char postexp[])
{
stack<char> op;
int i = 0, j = 0;
//op.top = -1;
while (*exp != '\0')
{
switch (*exp)
{
case '('://左括号进栈
op.push(*exp);
exp++;
break;
case ')'://将栈中“(”以前的运算符依次删除存入数组exp中,然后将“(”删除
while (!op.empty() && op.top() != '(')
{
postexp[i++] = op.top();
op.pop();
}
op.pop();
exp++;
break;
case '+':
case '-':
while (!op.empty() && op.top() != '(')
{//将栈中优先级大于等于‘+’或'-'的运算符退栈并存放到postexp中
postexp[i++] = op.top();
op.pop();
}
op.push(*exp);
exp++;
break;
case '*':
case '/':
while (!op.empty() && (op.top() == '*' || op.top() == '/'))
{//将栈中的“*”或者是“/”运算符依次出栈并存放到postexp中
postexp[i++] = op.top();
op.pop();
}
op.push(*exp);
exp++;
break;
case ' ':break;
default:
while (isdigit(*exp))
{
postexp[i++] = *exp;
exp++;
}
postexp[i++] = '#';
}
}
while (!op.empty())//最后的扫描工作
{
postexp[i++] = op.top();;
op.pop();;
}
postexp[i] = '\0';
cout << "后缀表达式" << endl;
for (j = 0; j < i; j++)
{
if (postexp[j] == '#')
j++;
cout << postexp[j];
}
cout << endl;
}
float compvalue(char postexp[])
{
stack<float> st;
float a, b, c, d;
while (*postexp != '\0')
{
switch (*postexp)
{
case '+':
a = st.top();
st.pop();
b = st.top();;
st.pop();;
c = a + b;
st.push(c);
break;
case '-':
a = st.top();
st.pop();
b = st.top();;
st.pop();;
c = b - a;
st.push(c);
break;
case '*':
a = st.top();
st.pop();
b = st.top();;
st.pop();;
c = a * b;
st.push(c);
break;
case '/':
a = st.top();
st.pop();
b = st.top();;
st.pop();;
if (a != 0)
{
c = b / a;
st.push(c);
}
else
{
cout << "除零错误!" << endl;
exit(0);
}
break;
default://进行最后的扫尾工作,将数字字符转换成数值存放到d中
d = 0;
while (isdigit(*postexp))
{
d = 10 * d + *postexp - '0';
postexp++;
}
st.push(d);
break;
}
postexp++;
}
return (st.top());
}
int main()
{
char exp[MaxSize] = "((18+6)*2-9)/2";
char postexp[MaxSize] = { 0 };
trans(exp, postexp);//exp存放中缀表达式,postexp存放后缀表达式
printf("后缀表达式的值\n");
printf("%.2f\n", compvalue(postexp));
return 0;
}
队列
排队?算了,用电脑来解释吧,cpu就是一个队列,一个执行完再到下一个,和队列一样先进先出也符合生活习惯,排在前面的优先出列
队列的专业术语和栈差不多类比吧
- 队头
- 队尾
- 出队
- 入队
下面我们来实现一下队列,还是用算法题来吧
排队
一个学校里老师要将班上NN个同学排成一列,同学被编号为1\sim N1∼N,他采取如下的方法:
先将11号同学安排进队列,这时队列中只有他一个人;
- 2−N号同学依次入列,编号为i的同学入列方式为:老师指定编号为i的同学站在编号为1\sim (i -1)1∼(i−1)中某位同学(即之前已经入列的同学)的左边或右边;
- 从队列中去掉M(M<N)M(M<N)个同学,其他同学位置顺序不变。
在所有同学按照上述方法队列排列完毕后,老师想知道从左到右所有同学的编号。
先分析:因为n还是比较大的(n<=100000),又因为要不停的插入和删除,所以我们可以用链表。读入每一个同学时,都把他左边和右边的同学更新;删除同学时,先把这个同学赋为0,再把他左边的同学连上右边的同学;最后找到排在最左边的同学,挨个输出。时间复杂度O(n)。
#include<cstdio>
#include<cstring>
int a[100010][3],n,m;
//a[i][2]表示学号为i的同学右边同学的学号
//a[i][3]表示学号为i的同学左边同学的学号
int main()
{
scanf("%d",&n);
int j=1;
memset(a,0,sizeof(a));
a[1][1]=1;
for(int i=2;i<=n;i++)
{
int x,y; scanf("%d %d",&x,&y);
a[i][1]=i;
if(y==0)
//插入左边
{
a[a[x][3]][2]=i; a[i][2]=x;
a[i][3]=a[x][3]; a[x][3]=i;
if(x==j) j=i;
//比较麻烦,要改链表
}
else
//插入右边
{
a[i][2]=a[x][2]; a[a[x][2]][3]=i;
a[x][2]=i; a[i][3]=x;
}
}
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
int x; scanf("%d",&x);
if(a[x][1]!=0)
//该同学还在
{
a[x][1]=0;
//踢掉
a[a[x][3]][2]=a[x][2];
a[a[x][2]][3]=a[x][3];
n--;
if(x==j) j=a[x][3];
}
}
int i=1,x=j;
while(i<=n)
{
printf("%d ",a[x][1]);
x=a[x][2]; i++;
}
return 0;
}
还有就是队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表
栈是仅在表尾(栈顶)进行插入和删除操作的线性表
又想起了我入门时候的一首小诗——来自大话数据结构
人生,就像是一个很大的栈演变。 出生时你赤条条地来到人世,慢慢地长大,渐浙地变老,最终还得赤条条地离开世间。
人生,又仿佛是一天一天小小的栈重现。童年父母每天抱你不断地进出家门,壮年你每天奔波于家与事业之间,老年你每天独自蹒珊于养老院的门里屋前。
人生,更需要有进栈出栈精神的体现。在哪里跌倒,就应该在哪里爬起来。无论陷入何等困境,只要抬头能仰望蓝天,就有希望,不断进取,你就可以让出头之日重现。困难不会永远存在,强者才能勇往直前。
人生,其实就是一个大大的队列演变。无知童年、快乐少年,稚傲青年,成熟中年,安逸晚年。
人生,又是一个又一个小小的队列重现。春夏秋冬轮回年,早中晚夜循环天天。变化的是时间,不变的是你对未来执着的信念。
人生,更需要有队列精神的体现。南极到北极,不过是南纬90º到北纬90º的队列,如果你中逢犹豫,临时转向,也许你就只能和企鹅相伴永远。可事实上,无论哪个方向,只要你坚持到底,你都可以到达终点。
好了感慨完,该开串了
串
串:由零个或多个字符串组成的有限序列,又称字符串
我打算就分俩个方面讲解
- 串的基本使用
- kmp算法
## 串的基本用法
串一般记为 s="absd"其中s是串的名字,c++中用双引号扩展起来的是串的内容,双引号不属于串的内容
还有几个概念:
- 空格串:区别于空串,空格串是有内容有长度的,只是都是空格,但是空串没有内容
- 子串与主串:串中任意个数的连续字符组成的子序列称为该串的子串,包含子串的串称为主串
像Lover中的over就是主串与子串的关系
## ASCII码
在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码),而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码,统一规定了上述常用符号用哪些二进制数来表示
在c++中char字符型变量,一个变量对应一个ascll码值
串的基本实现
实话实话,我感觉串的逻辑结构和线性表的很类似,不同之处是串针对的是字符集,但是代码实现还是有些不同之处,比如:查找
这就是一个串的查找,可以看到它自动给我补齐的操作,这也与之后的kmp算法有关
现在我先来用简单的傻瓜式搜索实现一下所有代码
//在c中许多是要单独写的,但是c++引入了string,其中包含了许多函数就不用自己直接写了,为了学习我先用c写,之后讲解c++的string
#include <iostream>
#include <string>
using namespace std;
int main ()
{
int size = 0;
int length = 0;
unsigned long maxsize = 0;
int capacity=0;
string str ("12345678");
string str_custom;
str_custom = str;
str_custom.resize (5);
size = str_custom.size();//大小
length = str_custom.length();//长度
cout << "size = " << size << endl;
cout << "length = " << length << endl;
return 0;
}
傻瓜模式匹配算法
int i = pos;
int j = 1;
while (i <= s[0] && j <= t[0])//当i<s的长度,且j<t的长度,循环继续
{
if (s[i] = t[j])
{
i++;
j++;//继续
}
else//缺点:后退重新匹配
{
i = i - j + 2;//i回到上次匹配首位的下一位
j = 1;//回到首位
}
}
if (j > t[0])
return i - t[0];//不匹配
else
return 0;
利用入门知识分析一下吧,最好的情况下,最好的结果是O(m),最坏的结果是O(n+m),取个平均是(n+m)/2,时间复杂度是O(n+m)
那么最坏的情况?就是每次不匹配成功都发生在串s,的最后一个字节上,这样的话,时间复杂度就成了O((n-m+1)*m)
KMP模式算法匹配
模式匹配的算法虽然简单但是实际上是非常糟糕的,实际上和算法中的动态规划有点像,动态规划就是减少了重复的计算,来换取高效,这里的kmp也一样,分析我们不难发现,模式匹配算法主要就是重复匹配太多了,kmp减少了重复的匹配来实现高效
正题:KMP算法是怎样优化这些步骤的。其实KMP的主要思想是:“空间换时间”,也和dp一样
首先,为什么朴素的模式匹配这么慢呢?
- 你再回头看一遍就会发现,哦,原来是回溯的步骤太多了。所以我们应该尽量减少回溯的次数。
- 怎样做呢?比如:goodgoole当字符’d’与’g’不匹配,我们保持主串的指向不变,
- 主串依然指向’d’,而把子串进行回溯,让’d’与子串中’g’之前的字符再进行比对。
- 如果字符匹配,则主串和子串字符同时右移。
上代码吧,作图能力实在不行,如果没有看明白,还请评论,我再讲解
typedef struct
{
char data[MaxSize];
int length; //串长
} SqString;
//SqString 是串的数据结构
//typedef重命名结构体变量,可以用SqString t定义一个结构体。
void GetNext(SqString t,int next[]) //由模式串t求出next值
{
int j,k;
j=0;k=-1;
next[0]=-1;//第一个字符前无字符串,给值-1
while (j<t.length-1)
//因为next数组中j最大为t.length-1,而每一步next数组赋值都是在j++之后
//所以最后一次经过while循环时j为t.length-2
{
if (k==-1 || t.data[j]==t.data[k]) //k为-1或比较的字符相等时
{
j++;k++;
next[j]=k;
//对应字符匹配情况下,s与t指向同步后移
//通过字符串"aaaaab"求next数组过程想一下这一步的意义
//printf("(1) j=%d,k=%d,next[%d]=%d\n",j,k,j,k);
}
else
{
k=next[k];
**//我们现在知道next[k]的值代表的是下标为k的字符前面的字符串最长相等前后缀的长度
//也表示该处字符不匹配时应该回溯到的字符的下标
//这个值给k后又进行while循环判断,此时t.data[k]即指最长相等前缀后一个字符**
//为什么要回退此处进行比较,我们往下接着看。其实原理和上面介绍的KMP原理差不多
//printf("(2) k=%d\n",k);
}
}
}
//kmp
int KMPIndex(SqString s,SqString t) //KMP算法
{
int next[MaxSize],i=0,j=0;
GetNext(t,next);
while (i<s.length && j<t.length)
{
if (j==-1 || s.data[i]==t.data[j])
{
i++;j++; //i,j各增1
}
else j=next[j]; //i不变,j后退,现在知道为什么这样让子串回退了吧
}
if (j>=t.length)
return(i-t.length); //返回匹配模式串的首字符下标
else
return(-1); //返回不匹配标志
}
算了算了,这样我都有点晕
看算法题目吧
kmp字符串匹配
#include<iostream>
#include<cstring>
#define MAXN 1000010
using namespace std;
int kmp[MAXN];
int la,lb,j;
char a[MAXN],b[MAXN];
int main()
{
cin>>a+1;
cin>>b+1;
la=strlen(a+1);
lb=strlen(b+1);
for (int i=2;i<=lb;i++)
{
while(j&&b[i]!=b[j+1])
j=kmp[j];
if(b[j+1]==b[i])j++;
kmp[i]=j;
}
j=0;
for(int i=1;i<=la;i++)
{
while(j>0&&b[j+1]!=a[i])
j=kmp[j];
if (b[j+1]==a[i])
j++;
if (j==lb) {cout<<i-lb+1<<endl;j=kmp[j];}
}
for (int i=1;i<=lb;i++)
cout<<kmp[i]<<" ";
return 0;
}
其实我们也可以发现, KMP算法之所以快,不仅仅由于它的失配处理方案,更重要的是利用前缀后缀的特性,从不会反反复复地找,我们可以看到代码里对于匹配只有一重循环,也就是说 KMP 算法具有一种“最优历史处理”的性质,而这种性质也是基于 KMP的核心思想的。
树
终于摆脱线性表了,线性表是一对一,但是树就不一样了,一对多的性质扑面而来,先看一下百度的说法吧,
树:它是由n(n≥1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
就用这张图来描述树的特征:
- 当n=0,就称为空树
- 有且只有一个称为根的结点,这里为A
- 当n>1时,其余结点可以分为m(m>0)个互不相交的有限集,其中每个集合又是一棵树,称为子树
- 举个例子:
是以B为结点的子树
下面我们来将结点分一下类:
- 树的结点包含一个数据结构及若干指向其子树的分支
- 结点拥有的子树称为结点的度
- 度为0的结点称为叶结点或终端结点
- 度不为0的结点称为非终端结点或分支结点
看图
结点的关系:
这块有点像我们的家庭关系,比较好理解
像上图A为B,C的双亲,B,C互为兄弟,对于#来说,D,B,A,都是它的祖先,反之A的子孙有B,D,#
其他相关概念,特定情况才会用到
引入了深度,可以说是有几层就有多少深度.
无序树:如果将树中结点的各子树看成从左到右都是没有次序,都可以随意互换,则称为无序树,反之为有序树
树的基本操作
双亲表示法
树真的太像人了,人可能暂时没有孩子但是一定有且只有一个父母,树也一样除了根结点外,其余每个结点,它不一定有孩子,但是一定有且只有一个双亲
/*
Project: Tree_parent(树-双亲表示法)
Date: 2019/02/25
Author: Frank Yu
基本操作函数:
InitTree(Tree &T) 参数T,树根节点 作用:初始化树,先序递归创建
InsertNode(Tree &T, TElemType node) 插入树的结点 参数:树T,结点node 作用:在双亲数组中插入结点,增加树的结点值
InsertParent(Tree &T, TElemType node1, TElemType node2)//插入双亲数组的双亲域 参数:树T ,结点node1,结点node2
//作用:使双亲数组中,node2对应的双亲域为node1的下标
GetIndegree(Tree &T, TElemType node) //得到某结点入度 参数:树T,结点node 结点不存在返回-1
GetOutdegree(Tree &T, TElemType node) //得到某结点出度 参数:树T,结点node 结点不存在返回-1
PreOrder(Tree T) 参数:树T,根节点下标 作用:先序遍历树
PostOrder(Tree T) 参数:树T,根节点下标 作用:后序遍历树
LevelOrder(Tree T)参数:树T 作用:层序遍历树
功能实现函数:
CreateTree(Tree &T) 参数T,树根节点 作用:创建树,调用InsertNode,InsertParent
Traverse(Tree T) 参数T,树根节点 作用:PreOrder InOrder PostOrder LevelOrder遍历树
*/
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<string>
#include<stack>
#include<queue>
#include<algorithm>
#include<iostream>
#define TElemType char
#define Max 100
using namespace std;
//树的结点数据结构
typedef struct TNode
{
TElemType data;//数据域
int parent; //双亲
}TNode;
//树的数据结构
typedef struct Tree
{
TNode parent[Max];
int NodeNum;
}Tree;
//********************************基本操作函数********************************//
//初始化树函数 参数:树T 作用:规定数据域为#,则为空,双亲为-1,则为空
void InitTree(Tree &T)
{
for (int i=0;i<Max;i++)
{
T.parent[i].data = '#';
T.parent[i].parent = -1;
}
T.NodeNum = 0;
}
//插入树的结点 参数:树T,结点node 作用:在双亲数组中插入结点,增加树的结点值
bool InsertNode(Tree &T, TElemType node)
{
if (node != '#')
{
T.parent[T.NodeNum++].data = node;//插入到双亲数组中
return true;
}
return false;
}
//插入双亲数组的双亲域 参数:树T ,结点node1,结点node2
//作用:使双亲数组中,node2对应的双亲域为node1的下标
bool InsertParent(Tree &T, TElemType node1, TElemType node2)
{
int place1, place2;
place1 = -1;place2 = -1;
for (int i=0;i<T.NodeNum;i++)//查找两点是否存在
{
if (node1 == T.parent[i].data)place1 = i;
if (node2 == T.parent[i].data)place2 = i;
}
if (place1 != -1 && place2 != -1)//两点均存在
{
T.parent[place2].parent = place1;
return true;
}
return false;
}
//得到某结点入度 参数:树T,结点node 结点不存在返回-1
int GetIndegree(Tree &T, TElemType node)
{
int place = -1;
for (int i = 0;i<T.NodeNum;i++)
{
if (T.parent[i].data == node)place = i;
}
if (place!=-1)//结点存在
{
if(T.parent[place].parent!=-1)return 1;//双亲只能有一个
else return 0; //根节点没有双亲,即没有入度
}
return -1;
}
//得到某结点出度 参数:树T,结点node 结点不存在返回-1
int GetOutdegree(Tree &T, TElemType node)
{
int place = -1;
int outdegree = 0;
for (int i = 0;i<T.NodeNum;i++)
{
if (T.parent[i].data == node)place = i;
}
if (place != -1)
{
for (int i = 0;i < T.NodeNum;i++)
{
if (T.parent[i].parent == place)outdegree++;
}
return outdegree;
}
return -1;
}
//先序遍历 参数:树T,根节点下标
void PreOrder(Tree T,int i)
{
if (T.NodeNum != 0)
{
cout << T.parent[i].data << " ";
for(int j=0;j<T.NodeNum;j++)
{
if(T.parent[j].parent==i)
PreOrder(T,j);//按左右先序遍历子树
}
}
}
//后序遍历 参数:树T,根节点下标
void PostOrder(Tree T,int i)
{
if (T.NodeNum != 0)
{
for (int j = 0;j<T.NodeNum;j++)
{
if (T.parent[j].parent == i)
PostOrder(T, j);//按左右先序遍历子树
}
cout << T.parent[i].data << " ";
}
}
//层序遍历 参数:树T
void LevelOrder(Tree T)
{
queue<TNode> q;//借助队列
if (T.NodeNum!=0)
{
TNode temp;//暂存要出队的结点
q.push(T.parent[0]);//根结点入队
while (!q.empty())//队列非空
{
temp = q.front();
q.pop();
cout<<temp.data<<" ";
for (int j = 0;j<T.NodeNum;j++)
{
if (T.parent[T.parent[j].parent].data == temp.data)//当前结点的父节点的数据域与弹出的相同
//因为temp没有保存下标,只能按这种方式比较,默认结点名称不同
q.push(T.parent[j]);//队列先进先出,先入左孩子
}
}
}
}
//**********************************功能实现函数*****************************//
//创建树,调用InsertNode,InsertParent
void CreateTree(Tree &T)
{
int nodenum = 0;
int parent;
TElemType node,node1,node2;
printf("请输入树的结点个数:");
cin >> nodenum;
parent = nodenum - 1;
printf("请输入树的结点名称(空格隔开):");
while (nodenum--)
{
cin >> node;
InsertNode(T,node);
}
printf("请输入树的结点间的双亲关系(一对为一双亲关系,A B表示A为B的双亲):\n");
while (parent--)
{
cin >> node1>>node2;
InsertParent(T,node1,node2);
}
printf("\n");
}
//入度
void Indegree(Tree &T)
{
TElemType node;
int indegree;
printf("请输入结点名称:\n");
cin >> node;
indegree = GetIndegree(T, node);
if (-1 != indegree)
cout << "该结点入度为:" << indegree << endl;
else
cout << "结点不存在。" << endl;
}
//出度
void Outdegree(Tree &T)
{
TElemType node;
int outdegree;
printf("请输入结点名称:\n");
cin >> node;
outdegree = GetOutdegree(T, node);
if (-1 != outdegree)
cout << "该结点出度为:" << outdegree << endl;
else
cout << "结点不存在。" << endl;
}
//遍历功能函数 调用PreOrder InOrder PostOrder LevelOrder
void Traverse(Tree T)
{
int choice;
while (1)
{
printf("********1.先序遍历 2.后序遍历*********\n");
printf("********3.层次遍历 4.返回上一单元*********\n");
printf("请输入菜单序号:\n");
scanf("%d", &choice);
if (4 == choice) break;
switch (choice)
{
case 1: {printf("树先序遍历序列:");PreOrder(T,0);printf("\n");}break;
case 2: {printf("树后序遍历序列:");PostOrder(T,0);printf("\n");}break;
case 3: {printf("树层序遍历序列:");LevelOrder(T);printf("\n");}break;
default:printf("输入错误!!!\n");break;
}
}
}
//菜单
void menu()
{
printf("********1.创建 2.某点入度*********\n");
printf("********3.某点出度 4.遍历*************\n");
printf("********5.退出\n");
}
//主函数
int main()
{
Tree T;
int choice = 0;
InitTree(T);
while (1)
{
menu();
printf("请输入菜单序号:\n");
scanf("%d", &choice);
if (5 == choice) break;
switch (choice)
{
case 1:CreateTree(T);break;
case 2:Indegree(T);break;
case 3:Outdegree(T);break;
case 4:Traverse(T);break;
default:printf("输入错误!!!\n");break;
}
}
return 0;
}
所用图
孩子表示法
顾名思义,就是每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们也把这种方法叫做多重链表表式法,有点像线性表中的链式表示法
那么这样的话,我这里就写伪代码了
//指针域的个数就等于树的度,其中树的度又等于树各个结点度的最大值
struct ChildNode
{
int data;
ChildNode*;
}
ChildNode *D;//d为最大结点
d.ChildNode;
不难看出这样的话,如果各个树度之间的差距不大,还可以,但是如果各个树度之间的差距很大,那么很浪费空间,原因是许多的结点域都是空的
孩子兄弟表示法
这个可以说是学二叉树的基础,有的兄弟可能要说了,为什么不是兄弟表示法?还要带上我的孩子一起?
因为可能存在下面这种情况,只有了兄弟,孩子没有办法往下延申了,那么如何孩子和兄弟一起开呢?
是这样的,任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的,记得不是堂兄弟昂,是亲兄弟,下面我们看图
观察后,我们可以发现每个结点最多有俩个孩子,也就是一个简单的二叉树,这也可以说是,孩子兄弟表示法最大的好处
struct Node
{
int data;
*firstchild,*ringtsib;
}
Node *Tree;
二叉树
恶魔来了,有关二叉树及其衍生的东西往往是树中的难点,下面来个大家讲个故事:还是以前的小苗,话说我以前刚看上小苗时,几乎没有人知道,但是我对我的兄弟说我看上了一个女孩,让他猜,但是我每次只回答”对“或”错“,然后就相当于降低了难度莫,身高,体重,魅力,头发等等,都可以用一个人来比较,这样的话又降低了难度,二叉树也是一样的,只不过它是通过比较大小来降低难度的,下面我们回归正题
特点:
- 每个结点最多有俩棵子树
- 左右子树都是有顺序的,不能任意颠倒
- 即使只有一棵子树,也要区分它是左子树还是右子树
一般情况下,有以下几种基本形态
- 空二叉树,没有办法画图了
- 只有一个根结点
- 根结点只有左子树
- 根结点只有右子树
- 根结点既有左子树又有右子树
再思考一下,如果有三个结点的二叉树,又有几种形态呢?
5种,怎么来的?先看图
由于他必须是有序的所以要单个计算,左右分开,加起来就是5种
下面来说几个特殊的二叉树:
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
- 斜树:有点像线性表,这个斜可以不分左右,所以更像线性表了
如何判断完全二叉树,下面是它的特征:
- 叶子结点只能出现在最下俩层、
- 最下层的叶子一定集中在左部的连续位置
- 倒数俩层,若有叶子结点,一定都在右部连续位置
- 如果结点度为一,则该结点只有左孩子
- 同样结点数的二叉树,完全二叉树的深度最小
下面我们来看一下,二叉树的存储结构吧,也分为顺序存储和链式存储
顺序存储
由于树是一对多的关系,顺序存储实现有点困难,但是二叉树是一种特殊的树,由于它的特殊性,顺序存储可以实现。
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
链表存储
由于二叉树每个结点最多有俩个孩子,所以给它设计一个数据域和俩个指针域,组成二叉链表
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
BinaryTreeNode *tree;
遍历二叉树
所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题。从而引出了次序问题,像码神刚刚结束的高考志愿问题,哪个城市,哪个学校,哪个专业,或者那个她,由于选择不同,所以最后的遍历次序也截然不同。
方法:
- 前序遍历——访问根结点的操作发生在遍历其左右子树之前。
从根开始先左后右
- 中序遍历——访问根结点的操作发生在遍历其左右子树之中。
中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树
- 后序遍历——访问根结点的操作发生在遍历其左右子树之后。
从左到右先子叶后结点的方式遍历访问左右子树,最后是根结点
- 层序遍历——设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
}BTNode;
typedef BTNode* QDataType;
// 链式结构:表示队列
typedef struct QListNode
{
struct QListNode* _next;
QDataType _data;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* _front;
QNode* _rear;
}Queue;
BTNode* CreateBTNode(BTDataType x);
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 初始化队列
void QueueInit(Queue* q);
// 队尾入队列
void QueuePush(Queue* q, QDataType data);
// 队头出队列
void QueuePop(Queue* q);
// 获取队列头部元素
QDataType QueueFront(Queue* q);
// 获取队列队尾元素
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
int QueueEmpty(Queue* q);
// 销毁队列
void QueueDestroy(Queue* q);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);
// 初始化队列
void QueueInit(Queue* q)
{
assert(q);
q->_front = q->_rear = NULL;
}
// 队尾入队列
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode *newnode = ((QNode*)malloc(sizeof(QNode)));
newnode->_data = data;
newnode->_next = NULL;
if (q->_rear == NULL)
{
q->_front = q->_rear = newnode;
}
else
{
q->_rear->_next = newnode;
//q->_rear = q->_rear->_next;
q->_rear = newnode;
}
}
// 队头出队列
void QueuePop(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
if (q->_front == q->_rear)
{
free(q->_front);
//free(q->_rear);
q->_front = q->_rear = NULL;
}
else
{
QNode *cur = q->_front->_next;
free(q->_front);
q->_front = cur;
}
}
// 获取队列头部元素
QDataType QueueFront(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->_front->_data;
}
// 获取队列队尾元素
QDataType QueueBack(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->_rear->_data;
}
// 获取队列中有效元素个数
int QueueSize(Queue* q)
{
assert(q);
int size = 0;
QNode* cur = q->_front;
while (cur)
{
++size;
cur = cur->_next;
}
return size;
}
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
int QueueEmpty(Queue* q)
{
assert(q);
return q->_front == NULL ? 1 : 0;
}
// 销毁队列
void QueueDestroy(Queue* q)
{
assert(q);
QNode *cur = q->_front;
while (cur)
{
QNode *next = cur->_next;
free(cur);
cur = next;
}
q->_front = q->_rear = NULL;
}
BTNode* CreateBTNode(BTDataType x)
{
BTNode *node = (BTNode*)malloc(sizeof(BTNode));
node->_data = x;
node->_left = NULL;
node->_right = NULL;
return node;
}
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
if (a[*pi] == '#')
{
return NULL;
}
BTNode *node = (BTNode*)malloc(sizeof(BTNode));
node->_data = a[*pi];
++*pi;
node->_left = BinaryTreeCreate(a, n, pi);
++*pi;
node->_right = BinaryTreeCreate(a, n, pi);
return node;
}
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root != NULL)
{
if ((*root)->_left) // 有左孩子
BinaryTreeDestory(&(*root)->_left); // 销毁左孩子子树
if ((*root)->_right) // 有右孩子
BinaryTreeDestory(&(*root)->_right); // 销毁右孩子子树
free(*root); // 释放根结点
*root = NULL; // 空指针赋NULL
}
}
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;
}
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->_left == NULL&&root->_right == NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
}
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->_data == x)
{
return root;
}
BTNode* ret=BinaryTreeFind(root->_left,x);
if (ret != NULL)
{
return ret;
}
ret = BinaryTreeFind(root->_right, x);
if (ret != NULL)
{
return ret;
}
return NULL;
}
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
{
//printf("NULL ");
return;
}
printf("%c ", root->_data);
BinaryTreePrevOrder(root->_left);
BinaryTreePrevOrder(root->_right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
//printf("NULL ");
return;
}
BinaryTreeInOrder(root->_left);
printf("%c ", root->_data);
BinaryTreeInOrder(root->_right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
//printf("NULL ");
return;
}
BinaryTreePostOrder(root->_left);
BinaryTreePostOrder(root->_right);
printf("%c ", root->_data);
}
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode *front = QueueFront(&q);
QueuePop(&q);
printf("%c ", front->_data);
if (front->_left)
{
QueuePush(&q, front->_left);
}
if (front->_right)
{
QueuePush(&q, front->_right);
}
}
}
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode *front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
{
break;
}
printf("%s ", front->_data);
if (front->_left)
{
QueuePush(&q, front->_left);
}
if (front->_right)
{
QueuePush(&q, front->_right);
}
}
while (!QueueEmpty(&q))
{
BTNode *front = QueueFront(&q);
QueuePop(&q);
if (front != NULL)
{
return 0;
}
}
return 1;
}
还有最后一个树——线索二叉树
它的出现还是为了简约成本,实际上想一下算法和数据结构存在就是为了节约时间或空间,这个是节约不用的空指针域,
空的左孩子指针指向该结点的前驱,空的右孩子指针指向该结点的后继。这种附加的指针值称为线索,带线索的二叉树称为线索二叉树。
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#define MAXSIZE 100
typedef char ElemType;
typedef enum
{
Link,/*指向孩子结点*/Thread/*指向前驱或后继*/
} PointerTag;
typedef struct Node
{
ElemType data;
struct Node *lchild;
struct Node *rchild;
PointerTag ltag,rtag;
}*BitThrTree,BitThrNode;
BitThrTree pre;
void CreateBitTree2(BitThrTree *T,char str[]);//创建搜索二叉树
void InThreading(BitThrTree p);//中序线索化二叉树
int InOrderThreading(BitThrTree *Thrt,BitThrTree T);//通过中序遍历二叉树T,使T中序线索化。Thrt是指向头结点的指针
int InOrderTraverse(BitThrTree T,int (*visit)(BitThrTree e));//中序遍历线索二叉树
int Print(BitThrTree T);//打印二叉树的结点和线索标志
BitThrNode* FindPoint(BitThrTree T,ElemType e);//在线索二叉树中查找结点为e的指针
BitThrNode* InOrderPre(BitThrNode *p);//中序前驱
BitThrNode* InOrderPost(BitThrNode *p);//中序后继
void DestroyBitTree(BitThrTree *T);//销毁二叉树
#include "LinkBiTree.h"
void CreateBitTree2(BitThrTree *T,char str[])//创建搜索二叉树
{
char ch;
BitThrTree stack[MAXSIZE];
int top = -1;
int flag,k;
BitThrNode *p;
*T = NULL,k = 0;
ch = str[k];
while(ch != '\0')
{
switch(ch)
{
case '(':
stack[++top] = p;
flag = 1;
break;
case ')':
top--;
break;
case ',':
flag = 2;
break;
default:
p = (BitThrTree)malloc(sizeof(BitThrNode));
p->data = ch;
p->lchild = NULL;
p->rchild = NULL;
if(*T == NULL)
{
*T = p;
}
else
{
switch(flag)
{
case 1:
stack[top]->lchild = p;
break;
case 2:
stack[top]->rchild = p;
break;
}
if(stack[top]->lchild)
{
stack[top]->ltag = Link;
}
if(stack[top]->rchild)
{
stack[top]->rtag = Link;
}
}
}
ch = str[++k];
}
}
void InThreading(BitThrTree p)//中序线索化二叉树
{
if(p)//左子树线索化
{
InThreading(p->lchild);
if(!p->lchild)//前驱线索化
{
p->ltag = Thread;
p->lchild = pre;
}
if(!pre->rchild)//后继线索化
{
pre->rtag = Thread;
pre->rchild = p;
}
pre = p;
InThreading(p->rchild);//右子树线索化
}
}
int InOrderThreading(BitThrTree *Thrt,BitThrTree T)//通过中序遍历二叉树T,使T中序线索化。Thrt是指向头结点的指针
{
if(!(*Thrt = (BitThrTree)malloc(sizeof(BitThrNode))))
{
exit(-1);//为头结点分配单元
}
(*Thrt)->ltag = Link;//修改前驱线索标志
(*Thrt)->rtag = Thread;//修改后继线索标志
(*Thrt)->rchild = *Thrt;//将头结点的rchild指针指向自己
if(!T)//如果二叉树为空,则将lchild指针指向自己
{
(*Thrt)->lchild = *Thrt;
}
else
{
(*Thrt)->lchild = T;//将头结点的左指针指向根结点
pre = *Thrt;//将pre指向已经线索化的结点
InThreading(T);//中序遍历进行中序线索化
/*将最后一个结点线索化*/
pre->rchild = *Thrt;//将最后一个结点的右指针指向头结点
pre->rtag = Thread;//修改最后一个结点的rtag标志域
(*Thrt)->rchild = pre;//将头结点的rchild指针指向最后一个结点
}
return 1;
}
int InOrderTraverse(BitThrTree T,int (*visit)(BitThrTree e))//中序遍历线索二叉树
{
BitThrTree p;
p = T->lchild ;//p指向根结点
while(p != T)//空树或遍历结束时,p == T
{
while(p->ltag == Link)
{
p = p->lchild ;
}
if(!visit(p))//打印
{
return 0;
}
while(p->rtag == Thread && p->rchild != T)//访问后继结点
{
p = p->rchild ;
visit(p);
}
p = p->rchild ;
}
return 1;
}
int Print(BitThrTree T)//打印二叉树的结点和线索标志
{
static int k = 1;
printf("%2d\t%s\t %2c\t %s\t\n",k++,T->ltag == 0 ? "Link":"Thread",T->data ,T->rtag == 1 ? "Thread":"Link");
return 1;
}
BitThrNode* FindPoint(BitThrTree T,ElemType e)//在线索二叉树中查找结点为e的指针
{
BitThrTree p;
p = T->lchild ;
while(p != T)
{
while(p->ltag == Link)
{
p = p->lchild ;
}
if(p->data == e)
{
return p;
}
while(p->rtag == Thread && p->rchild != T)
{
p = p->rchild ;
if(p->data == e)
{
return p;
}
}
p = p->rchild ;
}
return NULL;
}
BitThrNode* InOrderPre(BitThrNode *p)//中序前驱
{
if(p->ltag == Thread)//如果p的标志域ltag为线索,则p的左子树结点为前驱
{
return p->lchild ;
}
else
{
pre = p->lchild ;//查找p的左孩子的最右下端结点
while(pre->rtag == Link)//右子树非空时,沿右链往下查找
{
pre = pre->rchild ;
}
return pre;//pre就是最右下端结点
}
}
BitThrNode* InOrderPost(BitThrNode *p)//中序后继
{
if(p->rtag == Thread)//如果p的标志域rtag为线索,则p的右子树结点为后驱
{
return p->rchild ;
}
else
{
pre = p->rchild ;//查找p的右孩子的最左下端结点
while(pre->ltag == Link)//左子树非空时,沿左链往下查找
{
pre = pre->lchild ;
}
return pre;//pre就是最左下端结点
}
}
void DestroyBitTree(BitThrTree *T)//销毁二叉树
{
if(*T)
{
if(!(*T)->lchild)
{
DestroyBitTree(&((*T)->lchild));
}
if(!(*T)->rchild)
{
DestroyBitTree(&((*T)->rchild));
}
free(*T);
*T = NULL;
}
}
#include "LinkBiTree.h"
int main(void)
{
BitThrTree T,Thrt;
BitThrNode *p,*pre,*post;
CreateBitTree2(&T,"(A(B(,D(G)),C(E(,H),F)))");
printf("线索二叉树的输出序列:\n");
InOrderThreading(&Thrt,T);
printf("序列 前驱标志 结点 后继标志\n");
InOrderTraverse(Thrt,Print);
p = FindPoint(Thrt,'D');
pre = InOrderPre(p);
printf("元素D的中序直接前驱元素是:%c\n",pre->data);
post = InOrderPost(p);
printf("元素D的中序直接后驱元素是:%c\n",post->data);
p = FindPoint(Thrt,'E');
pre = InOrderPre(p);
printf("元素E的中序直接前驱元素是:%c\n",pre->data);
post = InOrderPost(p);
printf("元素E的中序直接后驱元素是:%c\n",post->data);
DestroyBitTree(&Thrt);
return 0;
}
哈夫曼树
到这数据结构算过去一半了,我们先来总结一下
图
终于树结束了,累坏我了,开始图
如果说树是一对多的话,那么图就是多对多。
都见过地图吧,像陕西就和山西连在了一起,还和内蒙古等等连,典型的多对多
如果说线性表和树的专业术语比较繁琐的话,那么图可能颠覆你的三观,——还有这么多的概念?介于图的概念比较多,我们先来放概念,再讲解
图按照有无方向分为有向图和无向图。有向图由顶点和弧构成,无向图由顶点和边检成。弧有弧尾和弧头之分。
图按照边或须的多少分为稀疏图和稠密图。如果任意两个顶点之间都存在边叫完全图有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图。
图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做度,有向图顶点分为入度和出度。
图上的边或弧上带权则称为网。
图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量。
无向图中连通且n个顶点n-1条边叫生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林。
概念很多,一一刨析
顶点
像树中的结点,聪明的彦祖一下就懂了,就是图中的数据元素嘛,但是有不同之处,线性表中有空表,树中有空树,,但是可没有空图这一说法,why?举个例子:像皇帝的新衣一样,你总不能拿一张纸给别人说只有聪明的人才能看到吧?还有就是在线性表中有线性关系,在树中有层的关系,那么在图中呢?在图中都有关系,一般来说我们将任意俩个顶点的关系用术语边来表示,边的集合可以为空
有向图,无向图
简单的来看就是图上没有箭头就为无向图,反之有箭头为有向图。
如果非要我解释的话,容我先引出无向边和有向边的概念
无向边:反之就是这条边无箭头,如果是A——D,用(A,D)来表示
有向边:简单来说就是这条边有箭头,还是A->D,但是要用<A,D>来表示,注意:不能用<D,A>表示,其中,这条有向边称为弧,A是弧尾,D是弧头也就是说箭头指向的为弧头
那么有向图和无向图就简单了,如果一个图都由有向边组成,则称为有向图,反之为无向图
下面再引出俩个概念:完全有向图,完全无向图,
完全有向图:
在有向图中如果任意俩个顶点都存在方向相反的俩条弧,为完全有向图
完全无向图:
在无向图中任意俩个顶点都存在边相连,则称为完全无向图
还有俩个计算边数的公式为:n(n-1)/2 and n(n-1),猜出来了吧,第一个是计算完全无向图的,第二个是计算完全有向图的
下面我们看俩个图,自己判断一下:
还有个稠密图和稀疏图,都是相对而言的,就像有钱一样,如果我和乞丐比的话,我比较有钱,但是如果我和马云来比的话,可能都没有办法比较。
顶点和边
下面我们把顶点和边串起来讲一下,他们之间也是有联系的
对于无向图来说,当俩个顶点通过一条边相连时,称为顶点相关联,顶点度是和另一个顶点相关联边的条数,顶点可称为互为邻结点
对于有向图来说,如果A->D称为顶点A邻接到顶点D,以A为头的弧为入度,以D为尾的为出度
再来说路径,路径的长度为路径的边或弧的数目,其中第一个顶点和最后一个顶点相同的路径称为回路或环,序列中顶点不重复出现的路径为简单路径,除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路,又称为简单回路或简单环
连通图
图中从一个顶点到达另一顶点,若存在至少一条路径,则称这两个顶点是连通着的。例如图 1 中,虽然 V1 和 V3 没有直接关联,但从 V1 到 V3 存在两条路径,分别是 V1-V2-V3 和 V1-V4-V3,因此称 V1 和 V3 之间是连通的。
无向图中,如果任意两个顶点之间都能够连通,则称此无向图为连通图。例如,图 2 中的无向图就是一个连通图,因为此图中任意两顶点之间都是连通的。若无向图不是连通图,但图中存储某个子图符合连通图的性质,则称该子图为连通分量。
强连通图
有向图中,若任意两个顶点 Vi 和 Vj,满足从 Vi 到 Vj 以及从 Vj 到 Vi 都连通,也就是都含有至少一条通路,则称此有向图为强连通图。如图 4 所示就是一个强连通图。
与此同时,若有向图本身不是强连通图,但其包含的最大连通子图具有强连通图的性质,则称该子图为强连通分量。
剩下的就是图有关的算法题了,用例题来说吧
- 图的遍历
给出N个点,M条边的有向图,对于每个点v,求A(v)表示从点v出发,能到达的编号最大的点。
按题目来每次考虑每个点可以到达点编号最大的点,不如考虑较大的点可以反向到达哪些点循环从N到1,则每个点i能访问到的结点的A值都是i
每个点访问一次,这个A值就是最优的,因为之后如果再访问到这个结点那么答案肯定没当前大了
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
#define MAXL 100010
int N, M, A[MAXL];
vector<int> G[MAXL]; //vector存图
void dfs(int x, int d) {
if(A[x]) return; //访问过
A[x] = d;
for(int i=0; i<G[x].size(); i++)
dfs(G[x][i], d);
}
int main() {
int u, v;
scanf("%d%d", &N, &M);
for(int i=1; i<=M; i++) {
scanf("%d%d", &u, &v);
G[v].push_back(u); //反向建边
}
for(int i=N; i; i--) dfs(i, i);
for(int i=1; i<=N; i++) printf("%d ", A[i]);
printf("\n");
return 0;
}
- 邻接矩阵
给定一个整数矩阵
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
const int Len = 200 * 200 * 5 + 5,Inf = 1e9;
int dep[Len],cnt = 1,head[Len],s,t,S,T,cur[Len],val[Len],a[205][205],sumh[205],suml[205],LL,RR,n,m,sum,ans,minn;
struct node
{
int next,to,w;
}edge[Len << 1];
void add(int from,int to,int w)
{
edge[++ cnt].to = to;
edge[cnt].w = w;
edge[cnt].next = head[from];
head[from] = cnt;
}
int BFS()
{
queue<int> q;
memset(dep , 0 , sizeof dep);
q.push(S);dep[S] = 1;cur[S] = head[S];
while(!q.empty())
{
int p = q.front() ; q.pop();
for(int e = head[p] ; e ; e = edge[e].next)
{
int to = edge[e].to;
if(!dep[to] && edge[e].w)
{
dep[to] = dep[p] + 1;
cur[to] = head[to];
if(T == to) return dep[T];
q.push(to);
}
}
}
return dep[T];
}
int dfs(int u,int In)
{
if(u == T) return In;
int Out = 0;
for(int e = cur[u] ; e && In > 0 ; e = edge[e].next)
{
cur[u] = e;
int to = edge[e].to;
if(edge[e].w && dep[to] == dep[u] + 1)
{
int res = dfs(to , min(In , edge[e].w));
In -= res;
Out += res;
edge[e].w -= res;
edge[e ^ 1].w += res;
}
}
return (!Out) ? dep[u] = 0 : Out;
}
int Clone(int x,int y){return (x - 1) * n + y;}
bool check(int res)
{
memset(head , 0 , sizeof head) ; cnt = 1;
memset(val , 0 , sizeof val);
sum = ans = 0;
for(int i = 1 ; i < n ; i ++)//处理行
{
int now = Clone(i , m) , L = sumh[i] - res , R = sumh[i] + res;//L = sumh[i] - res , R = sumh[i] + res
add(s , now , R - L) , add(now , s , 0);
val[now] += L , val[s] -= L;
}
for(int i = 1 ; i < m ; i ++)
{
int now = Clone(n , i) , L = suml[i] - res , R = suml[i] + res;
add(now , s , R - L) , add(s , now , 0);
val[s] += L , val[now] -= L;
}
for(int i = 1 ; i < n ; i ++)
{
int now = Clone(i , m);
for(int j = 1 ; j < m ; j ++)
{
int to = Clone(i , j);
add(now , to , RR - LL) , add(to , now , 0);
val[to] += LL , val[now] -= LL;
}
}
for(int i = 1 ; i < m ; i ++)
{
int now = Clone(n , i);
for(int j = 1 ; j < n ; j ++)
{
int to = Clone(j , i);
add(to , now , RR - LL) , add(now , to , 0);
val[now] += LL , val[to] -= LL;
}
}
for(int i = 1 ; i <= n * m + 1 ; i ++)
{
if(val[i] > 0) add(S , i , val[i]) , add(i , S , 0) , sum += val[i];
if(val[i] < 0) add(i , T , -val[i]) , add(T , i , 0);
}
while(BFS()) ans += dfs(S , Inf);
if(ans == sum) return true;
return false;
}
signed main()
{
minn = 1e9;
scanf("%d %d",&n,&m);
n ++ , m ++;
s = n * m , t = n * m + 1 , S = n * m + 2 , T = n * m + 3;
for(int i = 1 ; i <= n - 1 ; i ++)
for(int j = 1 ; j <= m - 1 ; j ++)
{
scanf("%d",&a[i][j]);
sumh[i] += a[i][j] , suml[j] += a[i][j];
}
for(int i = 1 ; i <= n - 1 ; i ++) minn = min(minn , sumh[i]);
for(int i = 1 ; i <= m - 1 ; i ++) minn = min(minn , suml[i]);
scanf("%d %d",&LL,&RR);
int l = 0 , r = minn,anss;
while(l <= r)
{
int mid = (l + r) >> 1;
if(check(mid)) anss = mid , r = mid - 1;
else l = mid + 1;
}
printf("%d\n",anss);
return 0;
}
来自:矩阵
再来一个深搜结束吧
- 深度优先遍历
给出N个点,M条边的有向图,对于每个点v,求A(v)表示从点v出发,能到达的编号最大的点
比较简单:反向建边 + dfs
按题目来每次考虑每个点可以到达点编号最大的点,不如考虑较大的点可以反向到达哪些点循环从N到1,则每个点i能访问到的结点的A值都是i每个点访问一次,这个A值就是最优的,因为之后如果再访问到这个结点那么答案肯定没当前大了
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
#define MAXL 100010
int N, M, A[MAXL];
vector<int> G[MAXL]; //vector存图
void dfs(int x, int d) {
if(A[x]) return; //访问过
A[x] = d;
for(int i=0; i<G[x].size(); i++)
dfs(G[x][i], d);
}
int main() {
int u, v;
scanf("%d%d", &N, &M);
for(int i=1; i<=M; i++) {
scanf("%d%d", &u, &v);
G[v].push_back(u); //反向建边
}
for(int i=N; i; i--) dfs(i, i);
for(int i=1; i<=N; i++) printf("%d ", A[i]);
printf("\n");
return 0;
}
深搜有了,不上广搜有点过意不去,算了再来个广搜
- 广搜
有一个 n×m 的棋盘,在某个点 (x,y) 上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步。
#include<iostream>//P1443
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<queue>//运用了队列
#include<cmath>
using namespace std;
const int dx[8]={-1,-2,-2,-1,1,2,2,1};
const int dy[8]={2,1,-1,-2,2,1,-1,-2};//8个方向
queue<pair<int,int> > q;
int f[500][500];//存步数
bool vis[500][500];//走没走过
int main()
{
int n,m,x,y;
memset(f,-1,sizeof(f));memset(vis,false,sizeof(vis));
cin>>n>>m>>x>>y;
f[x][y]=0;vis[x][y]=true;q.push(make_pair(x,y));
while(!q.empty())
{
int xx=q.front().first,yy=q.front().second;q.pop();//取队首并出队
for(int i=0;i<8;i++)
{
int u=xx+dx[i],v=yy+dy[i];
if(u<1||u>n||v<1||v>m||vis[u][v])continue;//出界或走过就不走
vis[u][v]=true;q.push(make_pair(u,v));f[u][v]=f[xx][yy]+1;
}
}
for(int i=1;i<=n;i++)
{for(int j=1;j<=m;j++)printf("%-5d",f[i][j]);printf("\n");}//注意场宽!!
return 0;
}
- 最短路径
实际上对于最短路径,让我看的话,它和深搜,广搜差不多
主要是这俩个大佬发明的东西
- 迪杰斯特拉算法
- 弗洛伊德算法
下面就按顺序来看一下
Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。最后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。
这个总体上是一个:广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树
#pragma once
#include<iostream>
#include<string>
using namespace std;
//邻接矩阵保存
//记录起点到每个顶点的最短路径的信息
struct Dis {
string path;
int value;
bool visit;
Dis() {
visit = false;
value = 0;
path = "";
}
};
class Graph_DG {
private:
int vexnum; //图的顶点个数
int edge; //图的边数
int **arc; //邻接矩阵
Dis * dis; //记录各个顶点最短路径的信息
public:
//构造函数
Graph_DG(int vexnum, int edge);
//析构函数
~Graph_DG();
// 判断我们每次输入的的边的信息是否合法
//顶点从1开始编号
bool check_edge_value(int start, int end, int weight);
//创建图
void createGraph();
//打印邻接矩阵
void print();
//求最短路径
void Dijkstra(int begin);
//打印最短路径
void print_path(int);
};
#include"Dijkstra.h"
//构造函数
Graph_DG::Graph_DG(int vexnum, int edge) {
//初始化顶点数和边数
this->vexnum = vexnum;
this->edge = edge;
//为邻接矩阵开辟空间和赋初值
arc = new int*[this->vexnum];
dis = new Dis[this->vexnum];
for (int i = 0; i < this->vexnum; i++) {
arc[i] = new int[this->vexnum];
for (int k = 0; k < this->vexnum; k++) {
//邻接矩阵初始化为无穷大
arc[i][k] = INT_MAX;
}
}
}
//析构函数
Graph_DG::~Graph_DG() {
delete[] dis;
for (int i = 0; i < this->vexnum; i++) {
delete this->arc[i];
}
delete arc;
}
// 判断我们每次输入的的边的信息是否合法
//顶点从1开始编号
bool Graph_DG::check_edge_value(int start, int end, int weight) {
if (start<1 || end<1 || start>vexnum || end>vexnum || weight < 0) {
return false;
}
return true;
}
void Graph_DG::createGraph() {
cout << "请输入每条边的起点和终点(顶点编号从1开始)以及其权重" << endl;
int start;
int end;
int weight;
int count = 0;
while (count != this->edge) {
cin >> start >> end >> weight;
//首先判断边的信息是否合法
while (!this->check_edge_value(start, end, weight)) {
cout << "输入的边的信息不合法,请重新输入" << endl;
cin >> start >> end >> weight;
}
//对邻接矩阵对应上的点赋值
arc[start - 1][end - 1] = weight;
//无向图添加上这行代码
//arc[end - 1][start - 1] = weight;
++count;
}
}
void Graph_DG::print() {
cout << "图的邻接矩阵为:" << endl;
int count_row = 0; //打印行的标签
int count_col = 0; //打印列的标签
//开始打印
while (count_row != this->vexnum) {
count_col = 0;
while (count_col != this->vexnum) {
if (arc[count_row][count_col] == INT_MAX)
cout << "∞" << " ";
else
cout << arc[count_row][count_col] << " ";
++count_col;
}
cout << endl;
++count_row;
}
}
void Graph_DG::Dijkstra(int begin){
//首先初始化我们的dis数组
int i;
for (i = 0; i < this->vexnum; i++) {
//设置当前的路径
dis[i].path = "v" + to_string(begin) + "-->v" + to_string(i + 1);
dis[i].value = arc[begin - 1][i];
}
//设置起点的到起点的路径为0
dis[begin - 1].value = 0;
dis[begin - 1].visit = true;
int count = 1;
//计算剩余的顶点的最短路径(剩余this->vexnum-1个顶点)
while (count != this->vexnum) {
//temp用于保存当前dis数组中最小的那个下标
//min记录的当前的最小值
int temp=0;
int min = INT_MAX;
for (i = 0; i < this->vexnum; i++) {
if (!dis[i].visit && dis[i].value<min) {
min = dis[i].value;
temp = i;
}
}
//cout << temp + 1 << " "<<min << endl;
//把temp对应的顶点加入到已经找到的最短路径的集合中
dis[temp].visit = true;
++count;
for (i = 0; i < this->vexnum; i++) {
//注意这里的条件arc[temp][i]!=INT_MAX必须加,不然会出现溢出,从而造成程序异常
if (!dis[i].visit && arc[temp][i]!=INT_MAX && (dis[temp].value + arc[temp][i]) < dis[i].value) {
//如果新得到的边可以影响其他为访问的顶点,那就就更新它的最短路径和长度
dis[i].value = dis[temp].value + arc[temp][i];
dis[i].path = dis[temp].path + "-->v" + to_string(i + 1);
}
}
}
}
void Graph_DG::print_path(int begin) {
string str;
str = "v" + to_string(begin);
cout << "以"<<str<<"为起点的图的最短路径为:" << endl;
for (int i = 0; i != this->vexnum; i++) {
if(dis[i].value!=INT_MAX)
cout << dis[i].path << "=" << dis[i].value << endl;
else {
cout << dis[i].path << "是无最短路径的" << endl;
}
}
}
#include"Dijkstra.h"
//检验输入边数和顶点数的值是否有效,可以自己推算为啥:
//顶点数和边数的关系是:((Vexnum*(Vexnum - 1)) / 2) < edge
bool check(int Vexnum, int edge) {
if (Vexnum <= 0 || edge <= 0 || ((Vexnum*(Vexnum - 1)) / 2) < edge)
return false;
return true;
}
int main() {
int vexnum; int edge;
cout << "输入图的顶点个数和边的条数:" << endl;
cin >> vexnum >> edge;
while (!check(vexnum, edge)) {
cout << "输入的数值不合法,请重新输入" << endl;
cin >> vexnum >> edge;
}
Graph_DG graph(vexnum, edge);
graph.createGraph();
graph.print();
graph.Dijkstra(1);
graph.print_path(1);
system("pause");
return 0;
}
弗洛伊德算法,我就使用伪代码来写了,
//总体上是一个二重循环加上一个三重循环,主要运用于所有顶点到所有顶点的最短路径问题
void ShortestPath FLOYD(MGraph G, PathMatrix &p[], DistanceMatrix &D){
//用Floyd算法求有向网中各对顶点v和w之间的最短路径P[v][w]及其带权长
//度D[v][w].若P[v][w][u]为TRUE,则u是从v到w当前求得最短路径上的顶点。
for(v = 0; v < G.vexnum; ++v) //各对结点之间初始已知路径及距离
for(w = 0; w < G.vexnum; ++w){
D[v][w] = G.arcs[v][w];
for(u = 0; u < G.vexnum; ++w) P[v][w][u] = FALSE;
if(D[v][w] < INFINITY){ //从v到w有直接路径
P[v][w][v] = TRUE; P[v][w][w] = TRUE;
}//if
}//for
for(u = 0; u < G.vexnum; ++u)
for(v = 0; v<G.vexnum; ++v)
for(w = 0; w<G.vexnum; ++w)
if(D[v][u] + D[u][w] < D[v][w]){ //从v经u到w的一条路径更短
D[v][w] = D[v][u] + D[u][w];
for(i = 0; i<G.vexnum; ++i)
P[v][w][i] = P[v][u][i] || P[u][w][i];
}//if
}//ShortestPath.FLOYD
图用网上十分流行的一句话来结尾,
世界上最遥远的距离是我找不到与你的最短路径 , 哭了
最后
码神,肝了一个周的时间,算是把数据结构肝完了吧,算法如果要展开来讲的话,感觉10w字都拦不住,数据结构就到这吧,原创不易,希望支持,如果有想看我手撕《🗡 ☞offer》或者更python还是java的,欢迎留言,我抽剩下的半个月来肝一个完整版。还有就是如果有哪一块写的有问题也欢迎指出!
最后感谢大家的支持,想要一个关注!点赞,评论!