数据结构定义
我们如何把现实中大量而复杂的问题以特定的数据类型(个体)和特定的存储结构(个体的关系)保存到主存储器(内存)中。以及在此基础上为实现某个功能(比如查找某个元素,删除某个元素,对元素进行排序等)而执行的相应操作,这个相应的操作也叫算法。
数据结构 = 个体的存储 + 个体的关系存储
算法 = 对存储数据的操作
算法定义
o 通俗的说算法是解题的方法和步骤
o 衡量算法的标准
o 时间复杂度:程序大概要执行的次数,而非执行的时间(时间和机器有关)。
o 空间复杂度:程序执行过程中大概所占用的最大内存空间。
o 难易程度:用易懂,避免过于复杂。
o 健壮性
线性表
定义:具有相同特性数据元素
的一个有限
序列,把所有节点(在逻辑上单个独立的个体)用一条直线串起来,中间的所有节点,前面和后面只有唯一一个节点,头结点只有前结点,后结点只有后节点。
线性表的存储结构:
1. 顺序存储结构:
定义:
顺序存储结构表中所有元素按照逻辑顺序,依次存储到指定的存储位置开始的一块连续的
存储空间中。如:数组
特性:
1.随机访问特性:只要知道0点的位置,就可以马上找到任何一个点的位置;
2.静态分布:存储分配只能预先进行,一旦分配好了,在对其的操作过程始终不变。
2. 链式存储结构:
定义:
n个节点离散分配(存储位置通常不连续),彼此通过指针相连,每个节点只有一个前驱节点同时每个节点只有一个后续节点,首节点没有前驱节点,尾节点没有后续节点。
特性:
1.不支持随机访问:房间散落存在,如果想要访问最后一个,必须访问完前面所有的房间才能访问到最后一个房间的位置。
2.结点的存储空间利用率相对顺序表稍低些:每个结点需要划出一部分空间来存储指定下一个结点的位置的指针。
3.动态分配:链表的结点可以散落在内存的任何位置,且不需要一次性地划分所有结点所需的空间给链表,而是需要几个结点就划分几个结点。
3.两种存储结构对比
1.插入与删除元素上:顺序表在做插入操作时要移动多个元素,即再插入位置右方所有的结点都需要往后移动一个位置。链表中,进行插入操作无需移动元素。
//顺序表插入一个元素
//首先找到这个元素的位置
int findnum(sqlist l,int e){
int i;
for(int i=0;i<l.length();i++){
if(e == l.data[i]) return i;
}
return -1;
}
//插入数据
void insertElem(Sqlist &l,int x)//因为l本身需要发生改变,所以采用引用的方式
{
int p,i;
p = findnum(l,x);
for(i=l.length();i>=p;i--){
l.data[i+1] = l.data[i];
}
l.data[p] = x;
++(l.leagth);
}
链表(离散结构)
链表的概念
专业术语:
o 首节点:存放第一个有效数据的节点
o 尾节点:存放最后一个有效数据的节点
o 头结点:位于首节点之前的一个节点,头结点并不存放有效的数据,也没有存放头结点的个数,头结点的数据类型和首节点类型是一样的。加头结点的目的主要是为了方便对链表的操作
o 头指针:指向头结点的指针变量
o 尾指针:指向尾节点的指针变量
o 确定一个链表需要几个参数:只需要一个头指针参数,因为我们通过头指针可以推算出链表的其他所有信息
链表的分类
:
- 单链表
带头结点的单链表:head->next 等于NULL时,链表为空
不带头结点的单链表:head 等于NULL时,链表为空
struct node{
typename data; //数据域(存放要存储的数据)
node *next; //指针域(指向下一个结点的地址)
} NODE, *PNODE;//NODE等价于struct Node,PNODE等价于struct Node *;
- 双链表
定义:
既可以从开始结点走到终端结点,也能从终端结点反向走到开始结点的单链表。即,相当于在单链表结点上添加一个指针域,指向当前结点的前驱,方便通过后继找到前驱,从而实现终端结点到开始结点的数据序列。
其为空的判断条件和单链表相同。
struct node{
int data;
node * prior; //指向前驱结点的指针
node * next; //指向后继结点的指针
};
- 循环单链表:
定义:
单链表的最后一个指针域(空指针)指向链表的第一个结点的指针:
如果链表是含有头结点的指针:为空就是head = head->next;
如果链表是没有头结点的指针:则head = NULL;
4.循环双链表:
定义:
将终端结点的next指针指向链表中的第一个结点,将链表中第一个结点的prior指向终端结点。
如果是有头结点的循环双链表:head->next==head->prior == head.
如果是没有头结点的循环双链表:head ==NULL
动态分配内存
# include <malloc.h> //包含了malloc函数
int len = 5;
int *pArr = (int *)malloc(sizeof(int)*len);
//表示我们的操作系统,要为我们的程序分配20个存储空间,可以进行读写
// malloc函数只返回第一个字节的地址,这样无法区分数组的类型,干地址
//因此需要用一个强制类型转换
node*p = (node*)malloc(sizeof(node));
//new 运算符
# include <cstdlib> //free函数
int *p = new int;
node *p = new node;
如果要分配固定大小的动态空间:
int *p = new int[len];
free(p)用于内存释放,防止内存泄漏
链表的操作
单链表创建
PNODE create_list()
{
PNODE pHead = (PNODE)malloc(sizeof(NODE));
PNODE pTial = pHead;
pTial->pNext = NULL;
int val;
int len = 0;
printf("请输入链表的长度为:");
scanf("%d",&len);
for(int i=0;i<len;i++)
{
printf("请输入第%d个链表元素值:",i+1);
scanf("%d",&val);
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if(pNew == NULL)
{
printf("该内存空间没有分配成功\n");
exit(-1);
}
pTial->pNext = pNew;
pNew->data = val;
pTial = pNew;
pTial->pNext = NULL;
}
return pHead;
}
单链表中插入元素
在位置3处插入一个数值为4的结点。
//要插入合法的链表位置,首先要判断插入的链表位置是否为负数,插入为的前一个位置是否为空。
//假设插入的位置需要从1开始
void insert_list(PNODE pHead,int pos,int val)
{
PNODE ptr = pHead;
int i = 0;
while(ptr != NULL&&i<pos-1)
{
i++;
ptr = ptr->pNext; //找到要插入元素位置的前一个指针
}
if(i>pos-1||ptr == NULL )
{
printf("数据插入的位置不合法!\n");
}
PNODE t = ptr;
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if(NULL == pNew)
{
printf("内存空间分配失败!");
}
pNew->data = val;
pNew->pNext = ptr->pNext;
ptr->pNext = pNew;
return ;
}
单链表删除元素
void del(node*head, int x){
node* p = head->next;
node *pre = head;
while(p!=null){
if(p->data == x){
pre->next = p->next;
delete(p);
p = pre->next;
}else{ //数据域不是x,把pre和p都后移一位
pre = p;
p = p->next;
}
总结:
- 删除节点:① p->pNext = p->pNext->pNext 仅仅这个方式可以实现删除节点的功能,但是这样会让内存泄露,无法释放p->pNext节点的内存。②因此在写①之前需要r = p->pNext ,然后在赋值后将 free(r);
- 插入节点:两种方法①r = p->pNext ,p->pNext =q , q->pNext=r;
- q->pNext = p->pNext, p->pNext = q; p是结点域的一个指针变量,而p->pNext表示的是p 指向结点结构体变量中的pNext这个成员本身,而其本身指向的是它的后续结点
双链表创建
void createlistr(DLNode *&l,int a[],n){
DLNode *s,*r;
L = (DLNode *)malloc(sizeof(DLNode));
L->next = NULL;
L->prior = NULL;
r = (DLNode*)malloc(sizeof(DLNode));
r = L; //使得头结点也是尾结点
for(int i=0;i<n;i++){
s = (DLNode *)malloc(sizeof(DLNode));
s->data = a[i];
r->next = s;
s->prior = r; //相对单链表的创建多了一个
r = s;
}
r->next = NULL;
}
双链表插入与删除
//插入主要算法
s->next = p->next;
s->prior = p;
p->next = s;
s->next->prior = s;//如果s->next == NUll ,这条就不需要写
//删除主要算法
s = p->next;
p->next = s->next;
s->next->prior = p;
free(s);
例题一:1032 Sharing (25分)
题目大意:
求两个链表的⾸个共同结点的地址。如果没有,就输出-1
分析:
⽤结构体数组存储, node[i]表示地址为i的结点, key表示值, next为下⼀个结点的地址, flag表示第⼀条链表有没有该结点
遍历第⼀条链表,将访问过的结点的flag都标记为true,当遍历第⼆条结点的时候,如果遇到了true的结点就输出并结束程序,没有遇到就输出-1
#include <cstdio>
#include <cstdlib>
const int mmax = 100010;
struct NODE{
char data;
int next;
bool flag;
NODE(){
flag = 0;
}
}node[mmax];
int main(){
int a,b,n;
int addnow,addnext;
char elm;
scanf("%d%d%d",&a,&b,&n);
while(n--){
scanf("%d %c %d",&addnow,&elm,&addnext);
node[addnow].data = elm;
node[addnow].next = addnext;
//如果使用MAP标记的话,会出现超时的情况
//或者采用直接结构体赋值的方方式
//node[addnow] = {elm,addnext,false};
}
int pos = a;node[pos].flag = true;//其实不需要考虑第一个结点是否相同
while(node[pos].next!=-1){
pos = node[pos].next;
node[pos].flag = true;
}
/*方法二
for(int i=a;i!=-1;i = node[i].next)
node[i].flag = true;
*/
pos = b;
if(node[pos].flag) printf("%05d",pos);
else{
while(!node[pos].flag&&pos!=-1){
pos = node[pos].next;
}
if(pos==-1) printf("-1\n");
else printf("%05d\n",pos);
}
return 0;
}
栈和队列
例题一:制作一个简单的计算器
题目大意:
输入是带空格的字符串,只包含+,-,*,/ 的非负整数计算法表达式,计算该表达式的值。每个测试用例整数和符号之间都用空格隔开,多组输入,最后结果保留两位小数。
测试数据:
输入:
5 + 2 * 3 / 49 -4 / 13
985211985 / 211
30 / 90 - 26 + 97 - 5 - 6-13/886+51/29+7987+5792
2/3*4
输出:
4.81
4599207.75
12178.21
2.67
#include <cstdio>
#include <iostream>
#include <string>
#include <queue>
#include <stack>
#include <map>
using namespace std;
struct node{
double n;
char p;
bool flag;
}temp;
queue <node> q;
stack <node> s;
map<char,int> op;
string str;
//将表达式按照一定的算数逻辑顺序存放到队列Q中
void inptq(){
int len = str.length();
for(int i=0;i<len;){
if(str[i]>='0'&&str[i]<='9'){
temp.flag = true;
temp.n = str[i++]-'0';
while(i<len&&str[i]>='0'&&str[i]<='9'){ //将单个整数求和
temp.n = temp.n*10 + (str[i]-'0');
i++;
}
q.push(temp);
}else{
temp.flag = false;
while(!s.empty()&&op[str[i]]<=op[s.top().p]){//根据算数的优先级放入队列中
q.push(s.top());
s.pop();
}
temp.p = str[i];
s.push(temp);
i++;
}
}
while(!s.empty()){
q.push(s.top());
s.pop();
}
}
double outans(){
node temp1,temp2;
while(!q.empty()){
temp = q.front();
q.pop();
if(temp.flag) s.push(temp);
else{
temp.flag = true;
temp2 = s.top();s.pop();
temp1 = s.top();s.pop();
if(temp.p=='-') temp.n =temp1.n-temp2.n;
else if(temp.p=='+') temp.n =temp1.n+temp2.n;
else if(temp.p=='*') temp.n =temp1.n*temp2.n;
else temp.n =temp1.n/temp2.n;
s.push(temp);
}
}
temp = s.top();
s.pop();
return temp.n;
}
int main(){
op['+']=op['-']=1;
op['*']=op['/']=2;
while(getline(cin,str)&&str!="0"){
int len = str.length();
for(int i=0;i<len;i++){
if(str[i]==' ') str.erase(i,1);
}
while(!s.empty()) s.pop();
inptq();
double ans = outans();
printf("%.2f\n",ans);
}
return 0;
}
例题二:1051 Pop Sequence
试题内容:
Given a stack which can keep M numbers at most. Push N numbers in the order of 1, 2, 3, …, N and pop randomly. You are supposed to tell if a given sequence of numbers is a possible pop sequence of the stack. For example, if M is 5 and N is 7, we can obtain 1, 2, 3, 4, 5, 6, 7 from the stack, but not 3, 2, 1, 7, 5, 6, 4.
input
Each input file contains one test case. For each case, the first line contains 3 numbers (all no more than 1000): M (the maximum capacity of the stack), N (the length of push sequence), and K (the number of pop sequences to be checked). Then K lines follow, each contains a pop sequence of N numbers. All the numbers in a line are separated by a space.
output
For each pop sequence, print in one line “YES” if it is indeed a possible pop sequence of the stack, or “NO” if not.
Sample Input
5 7 5
1 2 3 4 5 6 7
3 2 1 7 5 6 4
7 6 5 4 3 2 1
5 6 4 3 7 2 1
1 7 6 5 4 3 2
Sample Output
YES
NO
NO
YES
NO
方法一:
#include <cstdio>
#include <stack>
using namespace std;
int m,n,k;
int ipt[1010];
stack <int> s;
int main(){
scanf("%d%d%d",&m,&n,&k);
while(k--){
bool flag = true;
int base = 0;//记录当下最小能到达的值
while(!s.empty()) s.pop();
for(int i=0;i<n;i++){
scanf("%d",&ipt[i]);
}
for(int i=0;i<n;i++){
if(ipt[i]>base+m-s.size()||ipt[i]>n){//当栈中的数量大于了栈的容量的时候
printf("NO\n");
flag = false;
break;
}
else{
if(ipt[i]==base+1) base = ipt[i];//这表示push一个pop一个
else{
if(s.size()){
if(s.top()==ipt[i]){
s.pop();
continue;
}
else if(s.top()>ipt[i]){//当没有按照规定顺序输出的时候
printf("NO\n");
flag = false;
break;
}
}
for(int j=base+1;j<ipt[i];j++){
s.push(j);
}
base = s.top()+1;
}
}
}
if(flag) printf("YES\n");
}
return 0;
}
}
方法二:
#include <cstdio>
#include <stack>
using namespace std;
int m,n,k;
int ipt[1010];
stack <int> s;//用来存放按照随机升序顺序输入,按照ipt数组输入顺序弹出的一组数据
int main(){
scanf("%d%d%d",&m,&n,&k);
while(k--){
while(!s.empty()) s.pop();
for(int i=0;i<n;i++){
scanf("%d",&ipt[i]);
}
int now = 0;bool flag = true;
for(int i=1;i<=n+1;i++){
while(!s.empty()&&ipt[now]==s.top()){
s.pop();
now++;
}
if(!s.empty()&&ipt[now]<s.top()){//没有按照规定的顺序输出
flag = false;
printf("NO\n");
break;
}
else{
s.push(i);//如果没有匹配的就继续输入直达匹配或者不符合条件为止
if(s.size()>m){
flag = false;
printf("NO\n");
break;
}
}
}
if(flag) printf("YES\n");
}
return 0;
}
树
树的定义
专业定义:
- 有且只有一个称为根的节点
- 有若干个互不相交的子树,这些子树本身也是一颗树
通俗定义:
- 树是由节点和边(指针域)组成
- 每个节点只有一个父节点但可以有多个子节点
- 但有一个节点例外,该节点没有父节点,此节点称为根节点
专业术语:
- 节点,父节点: ,子节点
- 子孙,堂兄弟
- 深度:从根节点到最底层节点的层数称之为深度,根节点是第一层
- 叶子节点:没有子节点的节点
- 非终端节点:实际就是非叶子节点
- 度:含有最大子节点的个数
常考性质
- 树可以没有结点,这种情况下的树称为空树
- 树的层次(layer) 从根节点开始算,即根节点为第一层,根节点子树的根结点为第二层,以此类推
- 把结点的子树棵树成为结点的度(degree),而树中结点的
最大的度
称为树的度,也称为树的宽度。这三幅图种的度分别是,2,3,5; - 由于一条边链接两个结点,且树中不存在环,因此对有n个结点的树,边数一定是n-1。且
满足连通、边数等于顶点数减1 的结构一定是一棵树
- 叶子结点被定义为度为0的结点,因此当树中只有一个节点(即只有根节点)时,根结点也算作为叶子结点。
- 结点的深度(depth)是指从跟结点(深度为1)开始自顶向下逐层累加至该结点时的深度值;
- 结点的高度(height)是指从底层叶子结点(高度为1)开始自底向上逐层累加至该结点的高度值。树的深度是指树中结点的最大深度,树的高度是指树种结点的最大高度。对树而言,深度和高度是相等的。对于上图中三棵树的深度和高度分别是4、4、2。但是具体到某个结点来说深度和高度就是不一样相等的。
- 多棵树组合在一起称为森林(forest),即森林是若干棵树的结合
树的分类
• 一般树:任意一个节点的子节点的个数都不受限制
• 二叉树:
一般定义:
- 任意一个节点的子节点个数
最多两个
,且子节点的位置(左子树右子树的位置不能更改,有序树)不可更改
递归定义:
- 要么二叉树没有根节点,是一棵空树
- 要么二叉树是由根节点、左子树、右子树组成,且左子树和右子树都是二叉树。
- 一般二叉树
- 满二叉树:在不增加树层数的前提下,无法再多添加一个节点的二叉树就是满二叉树(每一层的节点数都是最大节点数)
- 完全二叉树:如果只是删除了满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树。(满二叉树是完全二叉树的一个特例)
完全二叉树采用二叉链表的存储结构外,还可以右方便的存储方法。对一棵
• 森林:n个互不相交的树的集合
树的存储
二叉树的存储
- 链式存储
struct node{
typename data;
node * lchild;
node * rchild;
}
//由于在二叉树建树前根节点不存在,因此其地址一般设为NULL;
node * root = NULL;
node * newNode(int v){
node * Node = new node;
Node->data = v;
Node->lchild = Node->rchild = NULL;//表示这个新结点暂时没有左右子树
return Node;
}
- 连续存储【完全二叉树】
优点:查找某个节点的父节点和子节点(也包括判断有没有子节点)
缺点:耗用内存空间过大
完全二叉树,如果给它的所有结点按从上到下,从左到右的顺序进行编号(从1开始),如图所示:
编号一定要从1开始,才能满足任何一个结点左孩子是2x,右孩子的编号一定是2x+1。完全二叉树可以通过建立一个大小为2k的数组来存放所有结点的信息,其中k为完全二叉树的最大高度,且1号位存放必须是根节点。
该数组中元素存放的顺序恰好为该完全二叉树的层序遍历序列。
判断某个结点是否为叶结点的标志为:
该结点(记下标为root)左子结点的编号root*2 大于结点总个数n(不需要判断右结点,因为右节点是比左结点序号大,如果存在了左节点那就已经不是叶子结点了,还去判断就没有意义,如果不存在左节点那就更不可能存在右结点了);判断某个结点是否为空结点的标志:
该结点下标root大于结点总个数n。
编号从0开始,则对于左孩子就是2index + 1,右孩子就是 2index + 2;
• 一般树的存储
o 双亲表示法:求父节点方便
o 孩子表示法:求子节点方便
o 双亲孩子表示法:求父节点和子节点都很方便
o 二叉树表示法:把一个普通树转化成二叉树来存储
- 具体转换方法:
设法保证任意一个节点的
左指针域指向它的第一个孩子
右指针域指向它的兄弟
只要满足此条件,就可以把一个普通树转化为二叉树。
一个普通树转化成的二叉树(根节点一定没有右子树)
• 森林的存储
先把森林转化为二叉树,再存储二叉树:
将相邻的父节点依次作为节点的右子树再对各父节点进行转化
二叉树树的操作
二叉树查找修改
使用递归完成查找修改操作。二叉树递归两个重要元素:递归式和递归边界。
递归式:当前结点的左子树和右子树分别递归
递归边界:是当前结点为空时到达死胡同。
相当于:先判断当前结点是否时需要查找的结点;如果是,则对进行修改操作;如果不是,则分别往该结点的左孩子和右孩子递归,知道当前结点为NULL为止。
void search(node *root ,int x,int newdata){
if(root == NULL){
return; //空树,死胡同(递归边界)
}
if(root->data == x){
root ->data = newdata; //找到数据域为x的结点,把它修改成newdata
}
search(root->lchild,x,newdata);//往左子树搜索x(递归式)
search(root->rchild,x,newdata);//往右子树搜索x(递归式)
}
二叉树的插入
结点的插入位置一般却决于数据域需要在二叉树中存放的位置(这与二叉树本身的性质有关),且对给定的结点来说,它二叉树的插入位置只会也有一个。
二叉树结点的插入位置就是数据域在二叉树中查找失败的位置。而由于这个位置是确定的,因此在递归查找的过程中一定是只根据二叉树的性质来选择左子树或右子树中的一棵子树进行递归,且最后到达空树(死胡同)的地方就是查找失败的地方,也就是结点需要插入的地方
。
//insert 函数将在二叉树中插入一个数据域为x的新结点
//注意根结点指针root要使用引用,否则插入不会成功
void insert(node* &root, int x){
if(root == NULL) {//空树,说明查找失败,也即插入位置(递归边界)
root = newNode(x);
return ;
}
if(由二叉树的性质,x应该插在左子树){
insert(root->lcahild,x);//往左子树搜索(递归式)
}else{
insert(root->rchild,x);
}
}
//如果函数中需要新建结点,即对二叉树的结构做出修改,就需要加引用;如果知识修改当前已有结点的内容,或仅仅是遍历树,就不需要加引用。
完全二叉树创建
由于完全二叉树当中的任何一个结点(设编号为x,其中根结点编号为1),其左孩子结点的编号一定是2x,而有孩子的结点的编号一定是2x+1。
完全二叉树的建立与二叉查找树结合,则可以根据任何一个插入的顺序的到一个独一无二的二叉树:
先将输入的数组序列,按照从小到大的排序,然后对数组进行中序递归建树:
int k = 0;
Node * inordercreat(int index){ //index 为该结点的编号
Node * root = new Node;
if(index > n) return NULL;
root ->lchild = inordercreat(index*2);
root ->data = ipt[k++]; // 按照排好序的输入序列进行赋值
root ->rchild = inordercreat(index*2+1);
return root;
}
//在调用的时候index一定要从1开始
inordercreat(1);
//对于如果结果仅仅只是需要输出层次遍历的话可以直接通过二叉树的index顺序进行输出
int CBT[mmax]; int k =0;
void inorder(int index){
if(index > n) return ;
inorder(index*2);
CBT[index] = ipt[k++];
inorder(index*2+1);
}
for(int i=1;i<=index;i++){
printf("%d",CBT[i]);
}
//如果输入的是层次遍历的完全二叉树顺序:CBT[MMAX]
//对与完全二叉树的后序遍历也不需要构建树
void postorder(int index){
if(index > n) return ;
postorder(index * 2);
postorder(index * 2 + 1);
printf("%d%s",CBT[index],index == 1 ? "\n" : " " );
遍历
- 先序遍历【先访问根节点】
o 先访问根节点
o 再先序访问左子树
o 最后先序访问右子树
先序遍历结果:ABDGHEICFJK
递归实现:
递归式:先序遍历的规则
递归边界:二叉树是一棵空树
void preorder(node * root){
if(root == NULL){
return ;//到达空树,递归边界
}
//访问根结点root,例如将其数据域输出
printf("%d\n",root->data);
//访问左子树
preorder(root->lchild);
//访问右子树
preorder(root->rchild);
}
- 中序遍历【中间访问根节点】
o 中序遍历左子树
o 再访问根节点
o 再中序遍历右子树
例子:
中序遍历结果:GDHBEIACJFK
void inorder(node *root){
if(root == NULL) {
return; //到达空树,递归边界
}
//访问左子树
inorder(root->lchild);
//访问根节点root,如果将其数据域输出
printf("%d\n",root->data);
printf("%d\n",root->data);
//访问右子树
inorder(root->rchild);
}
中序遍历总是把根节点放在左子树和右子树中间,因此只要知道根结点,就可以通过根节点在中序遍历序列的位置区分左子树和右子树
。
- 后续遍历【最后访问根节点】
o 先中序遍历左子树
o 再中序遍历右子树
o 最后遍历根节点
例子:
后序遍历结果:GHDIEBJKFCA
void postorder(node *root){
if(node == NULL){
return;//到达空树,递归边界
}
//访问左子树
postorder(root->lchild);
//访问右子树
postorder(root->rchild);
//访问根结点root,如果将其数据域输出
printf("%d\n",root->data);
特点:总是把根节点放在最后访问,因此对后续遍历序列来说,序列的最后一个一定是根结点。
- 层次遍历
指按照层次的顺序从根结点向下逐层进行遍历,且对同一层的结点从左到右遍历。
从根结点开始从上往下逐层遍历,而对同一层进行从左到右的遍历,因此该二叉树的层序遍历序列为ABCDEF
这个过程和BFS很像,二叉树中广度恰好体现在层次上,从根结点开始的广度优先搜索,基本思路如下:
- 将根结点root加入队列q.
- 取出对队首结点访问它
- 如果该结点有左孩子,将左孩子入队
- 如果该结点右有孩子,将右孩子入队
- 返回2,直到队列为空。
void LayerOrder(node *root){
queue<node*> q; //注意队列里是存地址
q.push(root); //将根结点地址入队
while(!q.empty){
node * now = q.front(); //取出队首元素
q.pop();
printf("%d",now->data); //访问队首元素
if(now -> lchild != NULL) q.push(now->lchild); //左子树非空
if(now -> rchild != NULL) q.push(now->rchild); //右子树非空
}
}
如果题目中要求计算出每个结点所处的层次,这时就需要在二叉树结点的定义中添加一个记录层次layer的变量
struct node{
int data; //数据域
int layer; //层次
node * lchild; //左指针域
node * rchild; //右指针域
}
void LayerOrder(node* root){
queue<node*> q;
root->layer = 1;
q.push(root);
while(!q.empty()) {
node * now = q.front(); //取出队首元素
q.pop();
printf("%d ",now->data); //访问队首元素
if(now->lchaild != NULL){ //左子树非空
now->lchild->layer = now->layer +1; //左孩子的层号为当前层号+1
q.push(now->lchild);
}
if(now->rchild != NULL){
now ->rchild->layer = now->layer +1; //有孩子层号为当前层号+1
q.push(now->rchild);
}
}
}
已知两种遍历序列求原始二叉树
通过先序和中序或者中序和后序我们可以还原出原始二叉树,但是通过先序和后序是无法还原出原始二叉树;
换句话,只有通过先序和中序或者中序和后序,我们才可以唯一的确定一个二叉树。
-
例子1:
先序:ABCDEFGH
中序:BDCEAFHG
求后序?
分析:按照先序的定义,A为最外层根节点,按照中序的定义和前面的结论可知BDCE为A节点的左子树节点,FHG为A节点的右子树,再依次按照两个遍历定义可以推出原始二叉树为:
那么此二叉树的后序为:DECBHGFA -
例子1:
先序:ABDGHCEFI
中序:GDHBAECIF
求后序?
分析:按照先序的定义得到A为最外层根节点,再根据中序结果可知GDHB为A的左子树,ECIF为A的右子树;B先出现在先序结果中可知B为左子树的根节点,再根据中序结果知B节点没有右子树,GDH均为B节点的左子树,再根据先序结果D先出现,知D为B左子树的根节点,再根据先序发现G在D的后面且中序中G在D的前面得出G为D左子树的根节点,那么D的右子树的根节点就是H了,依次类推A的右子树,得出原始二叉树为:
那么此二叉树的后序为:GHDBEIFCA
首先找到ink == pre1 ,这样就在中序序列中找到根结点。则左子树的结点个数为:numLeft = k-1。左子树的先序序列区间是[2,k],左子树中序序列区间[1,k-1];右子树的先序序列区间是[k+1,n],右子树的中序序列区间是[k+1,n],接着只需要往左子树和右子树进行递归构建二叉树即可。
递归过程中,当前先序序列的区间为[preL,preR],中序序列的区间为[inL,inR],那么左子树的结点个数:numleft = k - inL。左子树的先序序列区间就是[preL+1,preL + numLeft],左子树的中序序列区间[inL, k -1];右子树的先序序列区间是[preL + numLeft+1,preR], 右子树中序序列区间[k+1,inR]
//题目给出两个数组pre[mmax],in[mmax],它们分别的区间:[prel,prer],[inl,inr];
node * cread(int prel,int prer,inl,inr){
if(prel > prer){
return NULL;//先序长度小于等于0时,直接返回
}
node* root = new node; //新建一个新的结点,用来存放当前二叉树的根结点
root->data = pre[prel];//新结点的数据域为根结点的值
int k;
for(k=inl;k<inr;k++){
if(in[k] == pre[prel]){//在中序序列中找到in[k] == pre[l]的结点
break;
}
}
int leftnum = k-inl; //左子树的结点个数
//左子树的区间在先序中:[prel+1,prel+leftnum],左子树的区间在中序中:[inl,k-1];
root->lchild = creat(prel+1,prel+leftnum,inl,k-1);
//右子树的区间在先序中:[prel+leftnum+1,prer],右子树的区间在中序中:[k+1,inr]
root-> rchild = creat(prel+leftnum+1,prer,k+1,inr);
return root;
}
- 例子3:
中序:BDCEAFHG
后序:DECBHGFA
求先序?
分析:由后序结果知A为最外层根节点,再根据中序结果知BDCE为A节点的左子树,FHG为A的右子树;A的左子树中B最靠近A那么根据后序规则得出B为左子树的根节点,再根据中序结果B在结果的第一位,由中序规则知B没有左子树,DCE均为B的右子树,在DCE中后序结果C最靠近B,得出C为B的右子树的根节点,再依据中序结果知C前面是D后面是E得出D为C的左子树,E为C的右子树,同理可以推出A的右子树,得出原始二叉树为:
node * creat(int postl,int postr,int inl,int inr){
if(postl>postr) return NULL;
node * root = new node;
root->data = post[postr];
int k;
for(k=inl;k<inr;k++){
if(in[k]==post[postr]) break;
}
int numleft = k-inl;
root->lchild = creat(postl,postl+numleft-1,inl,k-1);
root->rchild = creat(postl+numleft,postr-1,k+1,inr);
return root;
}
例题应用
1020 Tree Traversals (25分)
题⽬⼤意:
给定⼀棵⼆叉树的后序遍历和中序遍历,请你输出其层序遍历的序列。这⾥假设键值都是互不相等的正整数。
#include <cstdio>
#include <queue>
using namespace std;
const int mmax = 32;
struct node{
int data;
node * lchild;
node * rchild;
};
int n,pre[mmax],in[mmax],post[mmax],cnt;
node * creat(int postl,int postr,int inl,int inr){
if(postl>postr) return NULL;
node * root = new node;
root->data = post[postr];
int k;
for(k=inl;k<inr;k++){
if(in[k]==post[postr]) break;
}
int numleft = k-inl;
root->lchild = creat(postl,postl+numleft-1,inl,k-1);
root->rchild = creat(postl+numleft,postr-1,k+1,inr);
return root;
}
void preorder(node * root){
if(root == NULL) return;
pre[cnt++] = root->data;
preorder(root->lchild);
preorder(root->rchild);
} // 增送一个结果为先序的
void leveroder(node * root){
queue <node*> q;
q.push(root);
while(!q.empty()){
node * newnode = q.front();
q.pop();
pre[cnt++] = newnode->data;
if(newnode->lchild!=NULL) q.push(newnode->lchild);
if(newnode->rchild!=NULL) q.push(newnode->rchild);
}
return;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&post[i]);
}
for(int i=1;i<=n;i++){
scanf("%d",&in[i]);
}
node *Node = new node;
Node = creat(1,n,1,n);
cnt = 0;
//preorder(Node);
leveroder(Node);
for(int i=0;i<n;i++){
if(i) printf(" ");
printf("%d",pre[i]);
}
return 0;
}
二叉树的静态表示法
在一些题目中已经给出了树的编号,以及树的指向,则需要使用静态表示法
struct node{
int data;
int lchild;
int rchild;
}root[maxn]; //结点数组,maxn为结点上限个数
操作
- 查找与修改
void search(int root,int x,int newdata){
if(root == -1) return; //用-1代替NULL ,空树或则和死胡同
if(Node[root].data == x){
Node[root].data = newdata;
}
search(Node[root].lchild,x,newdata);
search(Node[root].rchild,x,newdata);
}
- 插入
int index = 0;
int newNode(int v){ //分配一个Node数组中的结点给新的结点,index为下标
Node[index].data = v;
Node[index].lchild = -1;
Node[index].rchild = -1;
return index++;
}
void insert(int &root,int x){
if(root == -1){
root = newNode(x); //给root赋以新的结点
return;
}
if(由二叉树的性质x应该插在左子树){
insert(Node[root].lchild,x);
}else{
insert(Node[root].rchild,x);
}
}
- 建立二叉树
int creat(int data[],int n){
int root = -1;
for(int i=0;i<n;i++){
insert(root,data[i]); //将data[0]~data[n-1] 插入二叉树
}
return root; // 返回二叉树的根节点下标;
}
- 遍历
void preorder(int root){
if(root == -1) return;
printf("%d\n",Node[root].data);
preorder(Node[root].lchild);
preorder(Node[root].rchild);
}
void inorder(int root){
if(root == -1) return;
inorder(Node[root].lchild);
printf("%d\n",Node[root].data);
inorder(Node[root].rchild);
}
void postorder(int root){
if(root == -1) return;
postorder(Node[root].lchild);
postorder(Node[root].rchild);
printf("%d",Node[root].data);
}
void layerorder(int root){
queue <int> q;
q.push(root);
while(!q.empty()){
int now = q.front();
q.pop();
printf("%d",Node[now].data);
if(Node[now].lchild != -1) q.push(Node[now].lchild);
if(Node[now].rchild != -1) q.push(Node[now].rchild);
}
}
一般树
一般树节点个数不限且子结点没有先后次序的树,因此指针域不再是两个可能时无数个,因此需要用一个数组来表示:
struct node{
typename data; //数据域
vector <int> child; //指针域,存放所有子结点的下标
}Node[maxn];结点数组,maxn为结点上线个数
当需要新建一个结点时,按照顺序从数组中取出一个下标
int index = 0;
int newNode(int v) {
Node[index].data = v;
Node[index].child.clear(); // 清空子结点
return index++; //返回结点下标,并令index自增
}
树遍历
与二叉树的遍历相近似
- 先根遍历
void preorder(int root){
printf("%d ",Node[root].data]); // 访问根节点
for(int i=0; i<node[root].child.size();i++){
preorder(Node[root].child[i]);
}
}
- 层次遍历
void layerorder(int root){
queue <int> q;
q.push(root);
while(!q.empty()){
int front = q.front(); //取出队首元素
printf("%d ",Node[front].data);
q.pop();
for(int i=0;i < Node[front].data){ // 访问当前结点的数据域
q.push(Node[front].child[i]); //将当前结点的所有子结点入队
}
}
}
// 如果层次遍历中需要添加层次的内容
struct Node{
int data;
int layer;
vector <int> child;
}node;
void layerorder(int root){
queue <int> q;
q.push(root);
Node[root].layer = 1; //根节点的层次为1;
while(!q.empty()){
int front = q.front();
q.pop();
printf("%d",node[front].data);
for(int i=0;i<node[front].child.size();i++){
int ch = node[front].child[i];
node[ch].layer = node[front].layer + 1;
q.push(ch);
}
}
}
计算利润(销售供应树)
1079 Total Sales of Supply Chain
sum = 本金 (1+r)k;
sum = num*pow(1+r,k)
题意:销售供应树,树根唯一。在树根处货物的价格为P,然后从跟结点开始每往子结点走一层,该层的货物价格就将会在父亲节点的价格上增加r%,给出每个结点的货物量,求它们
#include <cstdio>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
const int mmax = 100010;
struct Node{
int amount,layer;
vector <int> child;
}node[mmax];
int n,idn,t;
double p,r;
long double sum = 0;
void dfs(int index){
if(index == -1){
return ;
}
for(int i=0;i<node[index].child.size();i++){
int ch = node[index].child[i];
node[ch].layer = node[index].layer + 1;
dfs(ch);
sum += node[index].amount * pow(1+r/100,node[index].layer);
}
}
int main(){
scanf("%d%lf%lf",&n,&p,&r);
node[0].layer = 0;
node[0].amount = 0;
for(int i=0;i<n;i++){
scanf("%d",&idn);
if(idn==0) {
scanf("%d",&t);
node[i].amount = t;
node[i].child.push_back(-1);
}else{
for(int j=0;j<idn;j++){
scanf("%d",&t);
node[i].child.push_back(t);
node[t].amount = 0;
}
}
}
dfs(0);
long double ans = p * sum;
printf("%.1Lf\n",ans);
}
树与DFS,BFS
1053 Path of Equal Weight (30分)
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int mmax = 110;
struct Node{
int wight,layer;
vector <int> child;
}node[mmax];
int n,nonleaf,obn,t;
int id,idn,idt[mmax],amount = 0;
vector <int> ans;
int cnt = 0;
void DFS(int index){
if(index != -1) {
ans.push_back(node[index].wight);
amount += node[index].wight;
}
if(index == -1){
if(amount == obn){
int len = ans.size();
for(int i=0;i<len;i++){
if(i) printf(" ");
printf("%d",ans[i]);
}
printf("\n");
}
return;
}
for(int j=0;j<node[index].child.size();j++){
DFS(node[index].child[j]);
if(ans.size() >node[index].layer){
amount -= ans[ans.size()-1];
ans.pop_back();
}
}
}
bool cmp(int a,int b){
return node[a].wight > node[b].wight;
}
int main(){
scanf("%d%d%d",&n,&nonleaf,&obn);
for(int i=0;i<n;i++){
scanf("%d",&t);
node[i].wight = t;
node[i].child.push_back(-1);
}
node[0].layer = 1;
for(int i=0;i<nonleaf;i++){
scanf("%d%d",&id,&idn);
node[id].child.clear();
for(int j=0;j<idn;j++){
scanf("%d",&idt[j]);
}
sort(idt,idt+idn,cmp);
for(int j=0;j<idn;j++){
node[id].child.push_back(idt[j]);
node[idt[j]].layer = node[id].layer + 1;
}
}
DFS(0);
return 0;
}
二叉查找树的基本操作
二叉查找树的定义
二叉查找树BST是一种特殊的二叉树,又称为排序二叉树,二叉搜索树,二叉排序树
- 要么二叉查找树是一棵空树
- 要么二叉查找树由根节点、左子树、右子树组成,其中左子树和右子树都是二叉查找树,且左子树所有结点的数据域
均小于或等于
根结点的数据域,右子树上所有结点的数据域大于
根结点的数据域
- 查找操作
//search 函数查找二叉树种数据域为x的结点
void search(node * root,int x){
if(root == NULL){
printf("search failed\n");
return;
}
if(x == root->data){
printf("%d\n",root->data);
}else if(x < root->data){
search(root->lchild,x);
}else{
search(root->rchild,x);
}
}
二叉树查找树种的数据域顺序总是左子树<根节点<右子树
- 插入操作
void insert(node * &root ,int x){
if(root == NULL) { //空树,说明查找失败,即插入位置
root = newNode(x);
return
}
if( x == root->data){ // 查找成功,说明结点已存在,直接返回
return;
}else if(x <root->data){
inseart(root-<lchild,x);
}else{
insert(root->rchild,x);
}
}
//二叉查找树的建立
//先后插入n个结点的过程,这和一般二叉树的建立是完全一样的
node* Create(int data[], int n){
node * root = NULL;
for(int i=0;i<n;i++){
insert(root,data[i]);
}
return root;
}
根据输入的序列创建二叉查找树
同一组数字,如果插入顺序不同,最后生成的二叉查找树是不同的:
1.{5,3,7,4,2,8,6}
2. {7,4,5,8,2,6,3}
//首先我们需要创建一个头结点
Node * root = NULL;
void creat(Node * &root,int data){ //只有加了引用才能真正改变root的树的结构
if(root == NULL) {
root = new Node;
root->data = data;
root->lchild = root->rchild = NULL;
return root;
}
if(data < root->data){
creat(root->lchild,data);
}else if(data > root->data){
creat(root->rchild,data);
}
}
for(int i=0;i<n;i++){
creat(root,ipt[i]);
}
删除
删除一个结点后,还需要满足二叉查找树;
前驱:比结点权值小的最大结点称该结点的前驱
后继:比结点权值大的最小结点称为该结点的后继。
显然,结点的前驱是该节点左子树中最右结点,后继就是该结点右子树的最左结点(从右子树根结点开始不断沿着lchild 往下直到lchild 为NULL时的结点
//寻找以root为根的树种最大或最小权值的结点
//寻找以root为根结点的树中的最大权值结点
node* findMax(node* root){
while(root->rchild != NULL){
root = root->rchild;
}
return root;
}
//寻找以root为根结点的树种的最小权值结点
node* findMin(node* root){
while(root->lchild != NULL){
root = root->lchild;
}
return root;
}
删除思想:
- 如果当前结点root 为空,说明不存在权值为给定x的结点,直接返回
- 如果当前结点的权值恰为给定的权值x,说明找到了想要删除的结点,此时进入删除处理
- 如果当前结点root 不存在左右孩子,说明为叶子结点,直接删除
- 如果当前结点root存在左孩子,那么在左子树中查找前驱pre,然后让pre的数据覆盖root ,接着在左子树中删除结点pre。
- 如果当前结点root存在右孩子,那么在右子树中查找后继next,然后让next的数据覆盖root,接着在右子树中删除next。
- 如果当前结点的权值大于给定的权值x,则在左子树中递归删除权值为x的结点。
- 如果当前结点root的权值大于给定的权值x,则在右子树中递归删除权值x的结点。
void deleteNode(node* &root,int x){
if(root == NULL) return; //不存在权值为x的结点
if(root->data == x){
if(root->lchild == NULL&&root->rchild == NULL){
root = NULL; //叶子结点
}else if(root->lchild != NULL){
node * pre = findMax(root->lchild);
root->data = pre->data;
deleteNode(root->lchild,pre->data); //在左子树中删除结点pre
}else{
node * next = findMin(root->lchild);
root->data = pre->data;
deleteNode(root->rchild,next->data);
}
}else if(root->data < x){
deleteNode(root->lchild,x); //在左子树中删除x
}else{
deleteNode(root->rchild,x); //在右子树中删除x
}
}
为了防止一直删除前驱或者后继导致生成一个高度不平衡的BST,交替删除,或则记忆H高度,删除高度高的。
对于,直接删除后继的方法就是,让root->lchild = next ->rchid;
并查集
并查集的定义
是一种维护集合的数据结构,并(Union),查(Find),Set(集合)这3个词。
- 合并:合并两个集合
- 查找:判断两个元素是否在一个集合。
用数组father[i] 来实现,表示元素 i 的父亲结点,而父亲结点本身也是这个集合内的元素(1<=i<=n)。
father[1] = 2 就是表示元素 1 的父亲结点时元素2.
father[i] == i , 说明元素 i 时该集合的根结点
。
对于同一个集合来说只存在一个根结点
,且将其作为所属集合的标识。
并查集的基本操作
初始化
//一开始,每个元素都是独立的一个集合,因此需要令所有father[i] = i;
for(int i=1;i<=N;i++){
father[i] = i; // 令fater[i]为-1也可
查找
由于规定同一个集合中只存在一个根结点,因此查找操作就是对给定的结点寻找其根结点的过程;
//递推方法
//findfather 函数返回元素x所在集合的根结点
int findefather(int x){
while(x != father[x]) {
x = father[x];
}
return x ;
}
int findfather(int x){
if(x == father[x]) return x; //如果找到根结点,则返回根结点编号x;
else return findfather(father[x]); //否则,递归判断x 的父亲结点是否时根结点
}
合并
一般题目会给出两个元素,要求这个两个元素的集合合并
首先需要判断这个两个元素是否子在一个集合中,如不在:
- 调用查找函数,分别找出两个元素a,b,的根结点,然后再判断跟结点是否相同
- 根据1获得了faa,fab,因此只需要把其中一个父亲结点指向另一个结点,father[faa] = fab, 反过来也行
void union(int a,int b){
int faa = findfather(a);
int fab = findfather(b);
if(faa != fab){
father[faa] = fab;
}
}
路径压缩
如果findFather 函数的目的就是查找根结点:
可以将一条单链式的写成树形的:
int findfater(int x){
int a = x;
while(x != father[x]){
x = father[x];
}
while(a != father[a]){
int z = a;
a = father[a];
father[z] = x;
}
}
堆
堆排序:
- 首先创建一个初始堆,从最后一个非叶子结点开始即n/2,倒着遍历到1,每个进行比较,直到所有的都是当下所在树的大顶堆。
- 将最后一个元素与堆顶元素交换位置,然后从堆顶进行比较,直到满足之前的条件。
- 最后重复二的操作,然后进行遍历。
//对heap数组在[low,high]范围进行向下调整
//其中low为与调整结点的数组下标,high一般为堆中最后一个元素的数组下标
void downAdjust(int low,int high){
int i = low,j = i*2;
while(j<=high){//存在孩子结点
if(j+1<=high && heap[j+1]>heap[j]){
j = j+1;
}
if(heap[j] > heap[i]){
swap(heap[j],heap[i]); //交换最大权值的孩子与与调整结点i
i = j;//保持i为与调整点,j为i的左孩子
j = i*2;
}else{
break;
}
}
}
//序列中元素的个数为n,完全二叉树的叶子结点个数为:n/2上去整,因此数组下标在[1,n/2下取整]
//于是从n/2下取整,对每个遍历到i进行[i,n]范围的调整
//建堆
void createheap(){
for(int i=n/2;i>=1;i--){
downadjust(i,n);
}
}
//删除堆顶元素
void deleteTop(){
heap[1] = heap[n--];//用最后一个元素覆盖堆顶元素,并让元素个数减1
downadjust(1,n);//向下调整堆顶元素
}
//往堆里添加一个元素
//先把添加的元素放在数组元素的最后,完全二叉树最后一个结点的后面
//然后向上调整操作
//对heap数组在[low,high]范围进行向上调整
//其中low一般设置为1,high表示预调整结点的数组下标
void upadjust(int low,int high){
int i= high,j = i/2; //i为与调整点,j为父亲结点
while(j >= low){ //父亲在[low,high]范围内
if(heap[i]>heap[j]){
swap(heap[i],heap[j]);
i = j;
j = i/2;
}else{
beak; //父亲权值比与调整结点i的权值大,调整结束
}
}
}
//堆排序:使用堆结构对序列进行排序。讨论递增排序的情况
//堆顶元素最大,取出堆顶元素后,让最后一个元素替换至堆顶,再进行一次针对栈顶元素的向下调整
void heapsort(){
createheap(); // 建堆
for(int i=n;i>1;i--){
swap(heap[i],heap[1]);
downadjust(1,i-1); //调整堆顶
}
}
哈夫曼树
树的带权路径长度:等于它所有叶子结点的带权路径长度之和。
哈夫曼树:最优二叉树:对于同一组叶子结点来说,哈夫曼树可以不唯一,但是最小带权路径长度一定是唯一的。
哈夫曼树的构建思想:反复选择两个最小的元素,合并,直到只剩下一个元素。
合并果子:采用优先队列的方法
//优先队列,小顶堆
/*初始状态下,将果堆的质量压入优先队列,
之后每次从优先队列顶部取出两个最小的数,
将它们相加并重新压入优先队列
(需要在外部定义一个变量ans,将相加的结果累加起来)
重复直到优先队列中只剩下一个数
此时就是得到了消耗的最小体力ans */
//合并果子
#include <cstdio>
#include <queue>
using namespace std;
//代表小顶栈的优先队列
/*是不同于先进先出队列的另一种队列。
每次从队列中取出的是具有最高优先权的元素。*/
//priority_queue<Type,Container,Compare>
// Type 为数据类型
// Container 为保存数据的容器, 必须是用数组实现的容器,比如 vector, deque 但不能用list。STL里面默认用的是 vector
// Compare为元素比较方式。比较方式默认用 operator< , 所以如果你把后面俩个参数缺省的话,优先队列就是大顶堆,队头元素最大。
// less<long,long> 表大顶堆
prio_queue<long long,vector<long,long>,greater<long,long>> q;
int main(){
int n;
long long temp,x,y,ans = 0;
scanf("%d",&n);
for(int i=0;i < n;i++){
scanf("%lld",&temp);
q.push(temp); //将初始重量压入优先队列
}
while(q.size()!=1){
x = q.top();
q.pop();
y = q.top();
q.pop();
q.push(x+y);
ans += x+y;
}
printf("%lld\n",ans);
return 0;
}
哈夫曼编码
对任意一棵二叉树来说,如果把二叉树上的所有分支进行编号,左分支为0,右分支为1,对任何一个叶子结点,其编号一定不会成为成为任何一个结点编号的前缀。
前缀编码:任何一个字符的编码都不是另一个字符的编码的前缀。其意义在于不产生混淆,让解码能够正常进行。
哈夫曼编码:
应用条件:确定的字符串,根据不同字符出现的个数建立哈夫曼树,然后再进行01编码,这是的编码长度实际上就是这棵树的带权路径长度。
图
图:由顶点
(Vertex) 和边
(Edge)组成,每条边的两端都必须是图的两个顶点(可以是相同的顶点)。记G(V,E)表示图G的顶点集为V,边集为E。
有些问题中,可以把无向图当作所有边都是正向和负向的两条有向边组成,这对解决一些问题很有帮助。
顶点的度
:指和该顶点相连的边的条数。
特别是对于有向图来说,顶点的出边条数称为顶点的出度
。顶点的入边条数称为该顶点的入度
。
顶点和边都可以有一定属性,而量化的属性称为权值,顶点的权值和边的权值分别称为点权
,边权
。
图的存储
邻阶矩阵
二位数组G[ ][ ]则被称为邻阶矩阵。如果G[i][j] = 1,则表示两顶点之间有边;如果G[i][j] = 0,则说明顶点i和顶点j之间不存在边权,对于存在的边,也剋令G[i][j]存放边权,对不存在的边可以设边权为0,-1,或者是一个很大的数。
邻接表
每个顶点都可能有若干条出边,如果把同一个顶点的所有出边芳再一个列表中,那么N个顶点就会有N个列表(如果没有出边,则对应空表)
也可以用vector来表示列表:
如果需要存放边的终点编号和边权,那么可以建立结构体Node
struct Node{
int v; //边的重点编号
int w; //边权
Node(int _v,int _w) : v(_v), w(_w) {} //构造函数
};
图的遍历
DFS遍历图
-
每一次都是沿着路径到不能再前进时才退回到最近的岔道口,并前往访问那些为访问分支顶点,直到遍历完整个图。
-
连通分量:
在无向图中,如果两个顶点之间可以互相到达(可以是通过路径间接到达),那么称这两个顶点连通。
如果图G(V,E)的任意两个顶点都连通,则称G为连通图;
否则,称G为非连通图,且称其中的极大连通子图连通分量。 -
强连通分量:
如果两个顶点可以各自通过一条有向路径到达另一个顶点,就称这两个顶点强连通。
如果图G(V,E)的任意两个顶点都强连通,则称图G为强连通图;否则,称图G为非强连通图,且称其中的极大强连通子图为强连通分量。
//DFS访问图,如果该图为连通图,则仅仅只需要一次DFS就可以访问所有顶点
DFS(u){ //访问顶点u
vis[u] = true; //设置u已被访问
for(从u除法能到达的所有顶点v){
if vis[v] == false; //如果v未被访问
DFS(V); //递归访问v
}
DFStrave(G){ //遍历图G
for(G的所有顶点u){ //对G的所有顶点u
if vis[u] == false; //如果u未被访问
DFS(u); //访问u所在的连通块
}
```cpp
// 邻接矩阵版本
const int maxv = 1000; //最大顶点数
const INF = 1000000000
int n,G[maxv][maxv];
bool vis[maxv] = {false};
void dfs(int u.int deapth){ //u为当前访问的顶点标号,depthwei shendu
vis[u] = true;
for(int v= 0;v<n;v++){
if(!vis[v]&&G[u][v]!=INF){
dfs(v,depth+1); //访问v,深度加1;
}
}
}
void dfstrave(){
for(int u=0;u<n;u++){
if(!vis[u]){
dfs(u,1);
}
}
}
```cpp
//邻接表
vector<int> Adj[mmax];
int n;
bool vis[mmax] = {false};
void dfs(int u,int depth){
vis[u] = true;
for(int i=0;i<adj[u].size();i++){
int v = Adj[u][i];
if(!vis[v]){
dfs(v,deapth+1);
}
}
}
void dfstrave(){
for(int u=0;u<n;u++){
if(!vis[u]){
dfs(u,1); //访问u和u所在的连通块,1表示初始为一层
}
}
}
BFS
BFS:
/*通过队列,反复通过取出队首顶点,
将该顶点可到达的未曾加入过的顶点,全部入队
直到队列为空为止*/
bfs(u){ //遍历u所在的连通块
queue q; //定义队列q,将u入队
inq[u] = true;
while(q非空){
取出q的队首元素u进行访问
for(从u除法可达的所有顶点v){
if(inq[v] == false){
将 v入队
inq[v] = true;
}
}
}
}
void bfstrave(){
for(G的所有顶点u) //枚举G的所有顶点u
if(inq[u] == false){
BFS(u); //遍历u 所在的连通块
}
}
//邻接矩阵版
int n,g[mmax][mmax] = {false};
void bfs(int u){
queue<int> q;
q.push(u);
inq[u] = true;
while(!q.empty){
int u = q.front();
q.pop();
for(int v=0;v<n;v++){
if(inq[v] == false && g[u][v] != INF){
q.push(v);
inq[u] = true;
}
}
}
}
void bfstrave(){
for(int u=0;u<n;u++){
if(!inq[u]){
bfs(u);
}
}
}
//邻接表版
vector <int> adj[mmax];
int n;
bool inq[maxv] = {false};
void bfs(int u){
queue <int> q;
q.push(u);
inq[u] = true;
while(!q.empty()){
int u = q.front();
q.pop();
for(int i=0;i<adj[u].size();i++){
int v = adj[u][i]
if(!inq[v]){
q.push(v);
inq[v] = true;
}
}
}
}
bfstrave(){
for(int i=0;i<n;i++){
if(!inq[i]){
bfs(i);
}
}
}
//如果需要考虑bfs的层次
struct node{
int v;
int layer;
};
void bfs(int s){
queue<node> q;
node start;
start.v = s;
start.layer = 0;
q.push(start);
inq[start.v] = true;
while(!q.empty()){
node topnode = q.front();
q.pop();
int u = topnode.v;
for(int i=0;i<adj[u],size();i++){
node next = adj[u][i];
next.layer = topnode.layer +1;
if(!inq[next.v]){
q.push(next);
inq[next.v] = true;
}
}
}
}
最短路径
Dijkstra 算法(迪杰斯特拉算法)
/*G为图,一般设成全局变量;数组d为源点到达各点的最短路径长度
s为起点*/
Dijkstra(G,d[],s){
初始化;
for(循环n次){
u = 使d[u]最小的还未被访问的顶点的标号;
记u已被访问;
for(从u出发到达的所有顶点v){
if(v未被访问&&以u为中介点使s到顶点v的最短距离d[v]更优) {
优化d[v];
}
}
}
//邻接矩阵版
//适用于点数不大(例如v不超过1000)的情况,相对好写
int n,G[mmax][mmax]; //n为顶点数,mmax为最大顶点数
int d[mmax]; //起点到达各点的最短路径长度
bool vis[mmax] = {false}; //标记数组,vis[i] == true 表示已访问
void dijkstra(int s){
fill(d,d+mmax,INF); //fill函数将整个d数组赋值为INF
d[s] = 0; //起点s到达自身的距离为0
for(int i = 0; i<n; i++){ //循环n次
int u = -1, min = INF; //u使d[u]最小,min存放该最小的d[u]
for(int j=0;j<n;j++){
if(vis[j] == false && d[j] < min){
u = j;
min = d[j];
} //选出中介点,即当下距离最短的点
}
//找到不到小于INF的d[u],说明剩下的顶点和起点s不连通
if(u == -1) return;
vis[u] = true; //标记已经访问的
for(int v=0;v<n;v++){
//如果v未访问&& u能到达v&&以u为中介点可以使d[v]更优
if(vis[v] == false && G[u][v] != INF && d[u]+G[u][v] < d[v]){
d[v] = d[u] + G[u][v]; //优化d[v]
}
}
}
}
if(v未被访问&&以u为中介点可以使得起点到顶点v的最短距离d[v]最优){
d[v]最优
令v的前驱为u; pre[v] = u; //用来前一个点到另一个点的最短路径方法
}
//通过递归的方式来求解距离
void dfs(int s,int v){ //s为起点编号,v为当前访问的顶点编号(从终点开始递归)
if(v == s){ //如果当前已经到达起点s,则输出起点并返回
printf("%d\n",s);
return;
}
dfs(s,pre[v]);
printf("%d\n",v); //从最深处return回来之后,输出每一层的顶点号 。
}
//用DFS来衡量djs的第二标尺最优值
int optvalue; //第二标尺最值
vector <int> pre[mmax];
vector <int> path,temppath;
void dfs(int v){ //v为当前访问结点
//递归边界
if(v == st){ //如果到达了叶子结点st
temppath.push_back(v); //将起点st加入临时路径temppath
int value; //存放临时路径tempath 的第二标尺
计算路径temppath 上的value值;
if(vlaue 优于 optvalue){
optvalue = value;
path = temppath;
}
temppath.pop_back(); //将刚加入的结点删除
return ;
}
//递归式
temppath.push_back(v);
for(int i=0;i<pre[v].size();i++){
dfs(pre[v][i]); //结点v的前驱结点pre[v][i]
}
temppath.pop_back();
}
//边权之和
int value = 0;
for(int i = temppath.size()-1;i>0;i--){
//当前结点id,下一结点idnext
int id = temppath[i], idnext = temppath[i-1];
value += v[id][idnext]; //value 增加边id -> idnext的边权
}
//点权之和
int value = 0;
for(int i=temppath().size()-1;i>=0;i--){
int id = temppath[i];
value += w[id]; //value 增加结点id的点权
}
//floyd算法
void floyd(){
for(int k = 0;k < n; k++){
for(int i = 0;i < n;i++){
for(int j = 0;j < n;j++){
if(dis[i][k] != INF&&dis[k][j] &&dis[i][k]+dis[k][i]<dis[i][j]){
dis[i][j] = dis[i][k] + dis[k][j];
}
}
}
}
}
动态规划
将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。
记忆话搜索
:
// 斐波那契数列递归图
//记忆化搜索方式
//为了不重复计算,首先让dp[mmax] 都赋值为-1;
int F(int n){
if(n==0 || n==1) return 1;
if(dp[n] != -1) return dp[n];
else{
dp[n] = F(n - 1) + F(n -2);;
return dp[n]; //返回F(n)的结果;
}
}
//数塔
//边界:
for(int j=1;j<=n;j++){
dp[n][j] = f[n][j];
}
//从第n-1层不断网上计算出dp[i][j]
for(int i = n-1;i>=1;i--){
for(int j = 1;j <= i;j++){
//状态转移方程
dp[i][j] = max(dp[i+1][j],d[i+1][j+1]) + f[i][j];
}
}
如果一个问题可以被分解成若干个子问题,且这些子问题会重复出现,那么称这个问题拥有重复子问题。
如果一个问题的最优解可以由子问题的最优解有效的构造出来,那么称这个问题拥有最优子结构。
最大连续子序列和
状态表达式:dp[i] 表示表示以A[i]作为末尾的连续序列的最大和
1.这个最大和的连续序列只有一个元素,即以A[i]开始到A[i]结束
2. 这个最大和的连续序列有多个元素,即从前面某处A[p]开始(p<i),一直到A[i]结尾。
//数塔
//边界:
dp[0] = A[0];
//状态转移方程
for(int j=1;j<n;j++){
dp[j] = max(A[i],dp[i - 1] + A[i]);
}
//要通过遍历每一个i得到最大的才是结果
for(int i=1;i<n;i++){
if(dp[i] > dp[k]){
k = i ;
}
}
}
//如何对目标序列进行指定序列的筛选,并且要求筛选的数量最大
//可以对指定序列进行映射转换:
memeset(hashtable, -1,sizeof(hashtable)); //整个数组初始化为-1
for(int i=0;i<m;i++){
scanf("%d",&x); //输入指定序列中的元素
hashtable[x] = i; //将指定序列按顺序银蛇到递增序列中
}
int num = 0; //用来存放指定序列的总数
scanf("%d",&l) //目标序列的长度
for(int t = 0; i< l;i++){
scanf("%d",&x); //输入目标序列中得到元素
if(hashtable[x] >= 0){ //若从目标序列中找到指定序列中的元素,则加入到数组中
A[num++] = hashtable[x];
}
}
//以下就可以采用LIS的方法进行 最长不下降子序列的操作。
//最长公共子串
//边界
for(int i=0;i <= lenA;i++){
dp[i][0] = 0;
}
for(int j = 0;j <= lenB;j++){
dp[0][j] = 0;
}
//状态转移方程
for(int i = 1;i <= lenA;i++){
for(int j = 1;j <= lenB;j++){
if(A[i] == B[i]){
dp[i][j] = dp[i-1][[j-1] +1;]
}else{
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
}
// dp[lenA][lenB] 就是答案。
勉励
加油ヾ(◍°∇°◍)ノ゙
笔记是平时刷题积累的,有借鉴很多大神的,例如柳神,郝斌老师,以及算法笔记,天勤数据结构等。
如果觉得我的文章对你有所帮助与启发,点赞给我个鼓励吧(づ ̄3 ̄)づ╭❤~
这篇文章会持续更新,关注我和我一起共勉加油
吧!
如果文章有错误,还望不吝指教!