数据结构笔记

随着复习进度持续更新
使用教材:严蔚敏数版据结构, 王道考研数据结构


前言

提示:本文仅为我近期学完后数据结构对自己所学的一些整理与个人理解,本人为菜狗,如有不足,欢迎指出,更欢迎有问题一起讨论


提示:以下是本篇文章正文内容

一、绪论

1.1 基本概念和术语

1.11认识数据结构

首先这门课叫做数据结构,我们需要知道什么是数据结构
数据结构:相互之间存在一种或多种特定关系的数据元素的集合
这种定义给我感觉就像是数学符号的定理,是终于反复推敲得到的严格定义所以觉得晦涩难懂是很正常的
而理解为 数据结构是带结构的数据元素的集合就比较简单易懂
而这里又提到了数据元素的概念,剩下一些基本概念个人觉得看如下这张图就可以理解
在这里插入图片描述
此外还有数据类型
你可以理解就是type
1)原子类型 值不可再分
2)结构类型 值可再分
3)数据抽象类型

1.12数据结构的要素

在对基本概念有了认知以后我们就可以讨论数据结构的基本三要素
逻辑结构
存储结构
数据的运算(严蔚敏版教材主要分为逻辑和存储两个层次)
1)逻辑结构
这里主要理解线性与非线性(线性即只存在1对1在这里插入图片描述
线性结构
在这里插入图片描述
非线性结构(并不是说只有1对n才是非线性,离散的点的集合也是非线性结构)在这里插入图片描述
2)存储结构
个人觉得刚学第一章时这部分很难理解
在学习完顺序表,链表,哈希表会好很多
1°顺序存储 存储单元格邻接,可以随机存取
2°链式存储 不要求相邻元素物理位置上邻接
3°索引存储
4°散列存储

1.2 算法与算法分析

算法的五大特性
1)有穷性
2)确定性
3)可行性
4)输入
5)输出
评价算法优劣的标准
1)正确性
2)可读性
3)健壮性
4)高效性
注意不要将特性与评判标准混淆
接下来是我认为第一章最重要的内容,也是考试的热点

算法效率的度量
算法效率的度量是通过时间复杂度和空间复杂度来描述的

1.21时间复杂度

所有需要了解几个符号
n 问题规模(算法求解问题输入量的多少)
f(n) 所有语句的频度之和(n的某个函数)
时间复杂度
                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                       T(n) = O(f(n))
O的含义是T(n)的数量级,表示同等数量级
这是书上的定义,可能看起来比较头大,不妨看一个简单的例子
E1.两个n阶矩阵的乘积算法

//注意这是伪代码
for(i = 1; i <= n; i++)		(1)	//频度为n + 1
	for(j = 1;j <= n; j++){	(2)	//频度为n*(n+1)
		c[i][j] = 0;			//频度为n^2
		for(k = 1;k <= n; k++)  //频度为n^2(n+1)
		c[i][j] = c[i][j] + a[i][k]*b[k][j];	//频度为n^3
	}

这里首先看单个语句的频度
(1)第一个for循环从1开始到n就是执行 n 次,但是注意,在这个循环结束时,会再i++一次,所以频度为n+1
(2)接下来看第二个for循环,当i=n时,内存循环了n次,外层循环了也是n次,此时是n^2,但是别忘记和(1)同理,i++还会再执行一次,所以内层为n,外层为n+1,所以结果为n*(n+1)
接下来都是同样的分析方法
所以得到结果
问题规模 n
语句频度和 f(n) = n+1+n(n+1)+n^ 2+ n^2(n+1) + n^3
                       ~~~~~~~~~~~~~~~~~~~~~~                        = 2n^3 + 3n^2 +2n+1
接下来T(n)也就是找f(n)的等价无穷小
原理即下面的公式
在这里插入图片描述
所以此算法的时间复杂度T(n) = O(n^3)
这里记住一个结论只考虑阶数高的部分
个人觉得可以理解只看最高项,不看常数项和系数,这么看主要是利用等价无穷小的思想

常见的渐进时间复杂度为
在这里插入图片描述
王道的口诀:常 对 幂 指 阶

在这里插入图片描述
多项相加只保留最高项,且系数变为1
在这里插入图片描述

多项相乘,都保留

初次以外
最坏时间复杂度
最好时间复杂度
平均时间复杂度
这些通过具体例子分析比较好理解

1.22空间复杂度

算法存储空间的度量,一般考的没有时间复杂度那么多,但是也很重要
                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                     S(n) = O(f(n))
在理解时间复杂度后,空间复杂度类似,就不赘述了,具体通过例子来说
但这里有个特殊的名词
O(1) 算法原地工作 即算法所需辅助空间为常量

二、线性表

个人觉得线性表是学习整个数据结构最重要的部分,因为后续章节多多少少会用到这一章节的内容
线性表:具有相同数据类型的n个数据元素的有限序列
你可以将线性表类比成数组
特点:
1)元素个数有限
2) 元素具有逻辑上的顺序性,表中元素有先后次序
3)元素的数据类型相同(每个元素占有相同的存储空间)

                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~                            线性表 L = (a1,a2,a3,…an)
a1是唯一的表头元素,位序从1开始,数组从0开始
an是唯一的表尾元素
除第一个元素外,每个元素有且仅有一个直接前驱
除最后一个元素外,每个元素有且只有一个直接后继

线性表有两种存储方式,即顺序存储和链式存储

2.1 顺序表

顺序表,即线性表的顺序存储
在这里插入图片描述
假设线性表存储的起始位置为Loc(a), sizeof(Elemtype)是每个元素所占的存储空间的大小
Elemtype为数据类型 ,sizeof(int) = 4B

数组内存的分配方式分为静态分配和动态分配
注意:动态分配不是链式存储,也是顺序存储,物理结构没有发生变化,依然是随机存储,只是分配的空间大小可以在运行时决定

接下来就是一些基本操作,根据自己的理解和编写习惯,每个人写的代码不一样,这里我就不再敲一遍了,有需要的可以私我。
1)插入
2)删除
3)按值查找

2.2 链表

三、栈和队列

栈和队列可以理解进行基本操作受限的线性表

3.1 栈

只允许一端进行插入删除操作的线性表
类似烤串,从最上面一块肉开始吃(LIFO 后进先出)
n个不同元素进栈,出栈元素不同排列的个数(卡特兰数)
在这里插入图片描述

3.11基本操作

存储方式:顺序栈,链栈
注意栈和队列的判空判满条件会因为实际给的条件不同而变化

下面给出顺序栈的代码,链栈类似单链表,有需要的可以私我

typedef struct {
	int data[MaxSize];   //静态数组存放栈中元素
	int top;			//栈顶指针
}SqStack;
//初始化栈
void InitStack(SqStack &S) {
	S.top = -1;			//初始化栈顶指针
}
//出栈
bool Pop(SqStack &S, int &x) {
	if (S.top == -1) //栈空报错
		return false;
	x = S.data[S.top];
	S.top = S.top - 1;
	//x = S.data[S.top--];
	return true;
}
//入栈
bool Push(SqStack &S, int x) {
	if (S.top == MaxSize - 1) //栈满报错
		return false;
	S.top = S.top + 1;	//指针+1
	S.data[S.top] = x;	//新元素入栈
	//这两句 等价 S.data[++S.top] = x
	return true;
}
bool StackEmpty(SqStack S) {
	if (S.top == -1)
		return true;
	else
		return false;
}

3.2 队列

只允许在表的一端插入,另一端删除的线性表
类似于食堂吃饭(FIFO 先进先出)

3.21基本操作

顺序存储

#define MaxSize 50
typedef struct{
ElemType data[MaxSize];
int front, rear;
}SqQueue

队空: Q.front== Q.rear == 0
进队: 队不满, 送值到队尾元素,再将队尾指针+1
出队: 队不空, 取队头元素,再将队头指针+1
注意Q.rear==Maxsize不能作为队列满的条件(假溢出)
假设此时rear=50,front=50,rear = Maxsize,但此时队列中只有一个元素

这里给出链式存储的代码
链式存储(判空 Q.rear = Q.front)

typedef struct LinkNode {
	int data;
	struct LinkNode *next;
}LinkNode;
typedef struct {
	LinkNode *front, *rear;
}LinkQueue;

//初始化
void InitQueue(LinkQueue &Q) {
	Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode));
	Q.front->next = NULL;
}
//出队
bool DeQueue(LinkQueue &Q, int &e) {
	if (Q.rear == Q.front)
		return false;
	LinkNode *p = Q.front->next;
	e = p->data;
	Q.front->next = p->next;
	if (Q.rear == p)
		Q.rear = Q.front;
	free(p);
	return true;
}
//入队
bool EnQueue(LinkQueue &Q, int e) {
	LinkNode *s = (LinkNode *)malloc(sizeof(LinkQueue));
	s->data = e;
	s->next = NULL;
	Q.rear->next = s;
	Q.rear = s;
	return true;
}

3.22循环队列

想要将一个队列变为一个环,显然需要引入mod运算
初始: Q.front == Q.rear == 0
进队:Q.front = (Q.front + 1) % MaxSize
出队:Q.rear = (Q.rear + 1) % MaxSize
队列长度:(Q.rear + MaxSize - Q.front)% MaxSize
牺牲了一个单元来区别队空与队满
队满:(Q.rear + 1) % MaxSize == Q.front
队空:Q.front == Q.rear
如果不想浪费空间可增加一个标值属性tag

3.3 表达式求值问题

3.31表达式的基本概念

三种表达式

中缀表达式 a+b 【操作数1 运算符 操作数2】
后缀表达式(逆波兰式)ab+【操作数1 操作数2 运算符】
前缀表达式(波兰式)+ab 【 运算符 操作数1 操作数2】

中缀转后缀
1、确定运算符顺序
2、【操作数1 操作数2 运算符】
3、有运算符没被处理就继续第2步
例如
1确定运算符顺序如下
A + B * ( C - D ) - E / F
   ~~    3    ~~    2        ~~~~~~        1      ~~~~      5    ~~    4
结果
A B C D - * + E F / -
            ~~~~~~~~~~~             1 2 3       ~~~~~       4 5

2确定运算符顺序如下
A + B * ( C - D ) - E / F
   ~~    5    ~~    3        ~~~~~~        2      ~~~~      4    ~~    1
结果
A B C D - * E F / - +
            ~~~~~~~~~~~             2 3       ~~~~~       1 4 5
这两种结果都是对的,但是第一种为计算机计算的结果,因为代码要求有确定性,并且计算机遵循左优先原则(只要左边的运算符能先计算,就优先计算左边的)

中缀转前缀
道理同上
区别就是遵循右优先原则(只要右边的运算符能先计算,就优先计算右边的)

3.32栈在表达式求值中的应用

个人认为这是这一章的一个难点,但考试时基本是考查手算
主要思想:
将中缀表达式转为后缀表达式进行计算
借助两个栈(操作数栈,运算符栈)
比较符号之间的优先级,进行二元运算

手算时个人比较喜欢使用的技巧,如
a+b-(cd - e)
从左向右依次扫描,当扫描到b-时,前一个运算符为+,并且+ -同级,所以中间的b,既要进行+又要-,再根据左优先原则,所以可以先进行左边的运算,即a+b。
而扫描到c
时,-*不同级,并且不知道后面有没有括号,此时就不能先进行运算了

可以通过代码体验一下(注意代码中的操作数只能是一位数,因为这里使用的是字符栈,如果想实现多位数运算,将操作数栈改为数栈,这个代码如果有时间就改进了发上来)
写法有很多种,这里严蔚敏版教材的代码
在这里插入图片描述

#include <stdlib.h>
#include <iostream>
using namespace std;

const char oper[7] = { '+','-','*','/','(',')','#' };	//一共有七种符号
#define MaxSize 100		//定义栈中元素的最大个数

typedef struct {
	int data[MaxSize];   //静态数组存放栈中元素
	int top;			//栈顶指针
}SqStack;

//栈的基本操作
void InitStack(SqStack &S) {
	S.top = -1;			//初始化栈顶指针
}
int GetTop(SqStack &S) {
	if (S.top == -1) //栈空报错
		return 0;
	return S.data[S.top];
}
bool Pop(SqStack &S, char &x) {
	if (S.top == -1) //栈空报错
		return false;
	x = S.data[S.top];
	S.top = S.top - 1;
	//x = S.data[S.top--];
	return true;
}
bool Push(SqStack &S, char x) {
	if (S.top == MaxSize - 1) //栈满报错
		return false;
	S.top = S.top + 1;	//指针+1
	S.data[S.top] = x;	//新元素入栈
	//这两句 等价 S.data[++S.top] = x
	return true;
}


bool In(char ch) {
	//判断是不是七个运算符中的一个
	for (int i = 0; i < 7; i++) {
		if (ch == oper[i]) {
			return true;
		}
	}
	return false;
}

char Operate(char first, char theta, char second) {
	//进行二元运算
	switch (theta) {
	case '+':
		//因为first和second是char类型,结尾有个'0',并且由于返回值类型是char,所以要加上48(ASCII)
		return (first - '0') + (second - '0') + 48;
	case '-':
		return (first - '0') - (second - '0') + 48;
	case '*':
		return (first - '0') * (second - '0') + 48;
	case '/':
		return (first - '0') / (second - '0') + 48;
	}
	return 0;
}

char Precede(char theta1, char theta2) {
	//两个()或者两个##
	if ((theta1 == '(' && theta2 == ')') || (theta1 == '#' && theta2 == '#')) {
		return '=';
	}
	//不能进行二元运算的情况,即theta2为/或*,因为不确定theta2后面的符号是什么,这里优先级比较,与手算时的想法不大一样
	else if (theta1 == '(' || theta1 == '#' || theta2 == '(' || (theta1
		== '+' || theta1 == '-') && (theta2 == '*' || theta2 == '/')) {
		return '<';
	}
	else
		return '>';
}

char EvaluateExpression() {
	//OPTR运算符栈,OPND操作数栈
	SqStack OPTR, OPND;
	char ch, theta, a, b, x;
	InitStack(OPND);
	InitStack(OPTR);
	Push(OPTR, '#');
	cin >> ch;
	while (ch != '#' || (GetTop(OPTR) != '#'))
	{
		if (!In(ch)) {
			//不是运算符进操作数栈
			Push(OPND, ch);
			cin >> ch;
		}
		else
			//比较运算符栈顶元素与当前输入的运算符的优先级
			switch (Precede(GetTop(OPTR), ch))
			{
			case '<':
				Push(OPTR, ch);
				cin >> ch;
				break;
			case '>':
				Pop(OPTR, theta);
				Pop(OPND, b);
				Pop(OPND, a);
				Push(OPND, Operate(a, theta, b));
				break;
			case '=':
				Pop(OPTR, x);
				cin >> ch;
				break;
			}
	}
	return GetTop(OPND);
}

int menu() {
	int c;
	cout << "0 - 9以内的多项式计算"<< endl;
	cout << "1.计算" << endl;
	cout << "0. 退出\n"<< endl;
	cout << "选择:";
	cin >> c;
	return c;
}

int main() {
	while (1) {
		switch (menu())
		{
		case 1:{
			cout << "请输入要计算的表达式(操作数和结果都在0 - 9的范围内,以#结束) :"<< endl;
			char res = EvaluateExpression();
			cout << "计算结果为" << res - 48 << endl <<endl;
			}
			break;
		case 0:
			cout << " 退出成功\n" << endl;
			exit(0);
		default:
			break;
		}
	}
	system("pause");
	return 0;
}

3.33栈在递归中的应用

函数调用时需要一个栈存储信息
函数调用特点LIFO
在这里插入图片描述
进制转换
在这里插入图片描述

#include <stdlib.h>
#include <iostream>
using namespace std;

#define MaxSize 10		//定义栈中元素的最大个数
typedef struct {
	int data[MaxSize];   //静态数组存放栈中元素
	int top;			//栈顶指针
}SqStack;
//初始化栈
void InitStack(SqStack &S) {
	S.top = -1;			//初始化栈顶指针
}
//出栈
bool Pop(SqStack &S, int &x) {
	if (S.top == -1) //栈空报错
		return false;
	x = S.data[S.top];
	S.top = S.top - 1;
	//x = S.data[S.top--];
	return true;
}
//新元素入栈
bool Push(SqStack &S, int x) {
	if (S.top == MaxSize - 1) //栈满报错
		return false;
	S.top = S.top + 1;	//指针+1
	S.data[S.top] = x;	//新元素入栈
	//这两句 等价 S.data[++S.top] = x
	return true;
}
bool StackEmpty(SqStack S) {
	if (S.top == -1)
		return true;
	else
		return false;
}

void conversation2(int N) {
	SqStack S;
	InitStack(S);
	while (N) {
		Push(S, N % 2);
		N = N / 2;
	}
	while (!StackEmpty(S)) {
		int x;
		Pop(S, x);
		cout << x;
	}
	return;
}

void conversation8(int N) {
	SqStack S;
	InitStack(S);
	while (N) {
		Push(S, N % 8);
		N = N / 8;
	}
	while (!StackEmpty(S)) {
		int x;
		Pop(S, x);
		cout << x;
	}
	return;
}

void conversation16(int N) {
	SqStack S;
	InitStack(S);
	while (N) {
		Push(S, N % 16);
		N = N / 16;
	}
	while (!StackEmpty(S)) {
		int x;
		Pop(S, x);
		if (x < 10)
			cout << x;
		else
			cout << char(x + 55);
	}
	return;
}

int main() {
	int N;
	cout << "输入一个十进制数N:";
	cin >> N;
	cout << "二进制为:";
	conversation2(N);
	cout << endl;
	cout << "八进制为:";
	conversation8(N);
	cout << endl;
	cout << "十六进制为:";
	conversation16(N);
	cout << endl;
	system("pause");
	return 0;
}

四、串

串我觉得可以直接理解成字符串string
存储方式:
1.定长存储
2.堆分配存储
3.块链存储

4.1 串的模式匹配

4.11BF算法

这个算法类似一个简单的遍历算法
时间复杂度O(nm)
n,m为主串和模式串的长度

但是 i = i - j + 2可能不太好理解
j             ~~~~~~~~~~~            子已经比较的长度
j-1          ~~~~~~~~          子串退回前一位
i-(j-1)      ~~~~      主串退回比较前
i-(j-1)+1 主串退回后向后退一位

#include<cstring>
#include<iostream>
using namespace std;
#define MAXSTRLEN 200   		
typedef char SString[MAXSTRLEN + 1];		//0号单元存放串的长度

int StrAssign(SString T,const char *chars) {
	//生成一个其值等于chars的串T
	int i;
	int len;
	if (strlen(chars) > MAXSTRLEN)
		return 0;
	else {
		len = strlen(chars);
		for (i = 1; i <= len; i++) {
			T[i] = *(chars + i - 1);
		}
		return len;
	}
}

int Index_BF(SString S, SString T, int lens, int lent) {
	int i = 1, j = 1;
	while (i <= lens && j <= lent) {
		if (S[i] == T[j]) {
			i++;
			j++;
		}
		else {
			i = i - j + 2;
			j = 1;
		}
	}
	if (j > 3)
		return i - 3;
	else
		return 0;
}

int main()
{
	int lens, lent;
	SString S;
	lens = StrAssign(S, "aaabbaba");
	SString T;
	lent = StrAssign(T, "abb");
	cout << "在第" << Index_BF(S, T, lens, lent) << "位匹配成功" << endl;
	system("pause");
	return 0;
}

4.11KMP算法

重点是next和nextval数组的求法
当第j个字符匹配失败,由前1~j-1个字符组成的串记为S
则:next[j] = S的最长相等前后缀长度 + 1
next[0] = 0
next[1] = 1
后面根据实际情况,记住是当前字符前的字符串
例如             ~~~~~~~~~~~            a b a b a a a b a b a a
next数组为 0 1 1 2 3 4 2 2 3 4 5 6
假设求next[4]
前面的串S为 a b a 注意不是abab
nextval数组就是对next数组的优化
从左向右,nextval[1] = 0
依次比较nextval[next[j]]和next[j],
若相等,则nextval[next[j]] = next[j]
若不等,则nextval[j] = next[j]

#include<cstring>
#include<iostream>
using namespace std;

#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;
#define MAXSTRLEN 255 //用户可在255以内定义最长串长
typedef char SString[MAXSTRLEN + 1]; //0号单元存放串的长度

Status StrAssign(SString T, const char *chars) { //生成一个其值等于chars的串T
	int i;
	if (strlen(chars) > MAXSTRLEN)
		return ERROR;
	else {
		T[0] = strlen(chars);
		for (i = 1; i <= T[0]; i++)
			T[i] = *(chars + i - 1);
		return OK;
	}
}

//计算next函数值
void get_next(SString T, int next[])
{ //求模式串T的next函数值并存入数组next
	int i = 1, j = 0;
	next[1] = 0;
	while (i < T[0])
		if (j == 0 || T[i] == T[j])
		{
			++i;
			++j;
			next[i] = j;
		}
		else
			j = next[j];
}//get_next

 //KMP算法
int Index_KMP(SString S, SString T, int pos, int next[])
{ // 利用模式串T的next函数求T在主串S中第pos个字符之后的位置的KMP算法
//其中,T非空,1≤pos≤StrLength(S)
	int i = pos, j = 1;
	while (i <= S[0] && j <= T[0])
		if (j == 0 || S[i] == T[j]) // 继续比较后继字
		{
			++i;
			++j;
		}
		else
			j = next[j]; // 模式串向右移动
	if (j > T[0]) // 匹配成功
		return i - T[0];
	else
		return 0;
}

int main()
{
	SString S;
	StrAssign(S, "aaabbaba");
	SString T;
	StrAssign(T, "abb");
	int *p = new int[T[0] + 1]; // 生成T的next数组
	get_next(T, p);
	cout << "主串和子串在第" << Index_KMP(S, T, 1, p) << "个字符处首次匹配\n";
	return 0;
}

4.2 数组

4.21 数组元素的地址

一维数组[0…n-1]
Loc(ai) = Loc(a0) + i*L
二维数组[0,h1][0,h2]

行优先
Loc(ai,j) = Loc(a0,0) + [i*(h2+1) + j]*L

列优先
Loc(ai,j) = Loc(a0,0) + [j*(h1+1) + i]*L

4.22 矩阵压缩存储

1、对称矩阵
只存储主对角线+下三角区
或者只存储主对角线+上三角区
主对角线上的元素容易被遗忘
行优先原则将各元素存入一维数组
数组大小为 (1+n)*n/2
第一行1个,第二行2个…
重点是将矩阵下标转换为一维数组下标
行优先原则
aij是第 [1+2+…+(i-1)] + j个元素(数组下标从1开始)
若下标从0开始要 -1
在这里插入图片描述
列优先原则
aij是第[n+n-1+…+n-j+2] + i-j +1个元素
第1列n个元素
第2列n-1个元素
第j-1列n-[(j-1)-1]个元素
2、三角矩阵
3、三对角矩阵
看起来可能不太明显,可以看例题

4.3 广义表

广义表是线性表的推广
A =()空表,长度为0
B =(e)长度为1
C =(a,(b,c,d))长度为2
D = (A, B,C) 长度为3 ,3个元素是广义表
E =(a,E)递归表,长度为2,相当于无限的广义表
(1)GetHead(),取出表头为非空广义表的第一个元素,可以是原子,也可以是子表
(2)GetTail() ,取出表尾为去掉表头外,其他元素构成的表,表尾一定是一个广义表
GetHead(B)= e
GetTail(B)= ()
GetHead(D)= A
GetTail(D)= (B,C)
(())与()不同,前者为空表,后者长度为2,可分解成得到的表头和表尾均为()
广义表深度即括号层数
详见例题

4.4 例题

选择题
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
应用题
例1.
在这里插入图片描述
每个元素32个二进位
主存储器字长为16位,所以每个元素占2字长
1、一共11列11行,每个元素占2字长 11x11x2 = 242
2、第四列 共11个元素,每个元素占2字长 11x2 = 22
3、按行,行下标是从-1
Loc = S + 8x11x2 + 4x2 - 1x2 = S +182
4、按列,同样注意行下标是从-1
Loc = S + 6x11x2 + 6x2 -1x2 = S + 142

例2.
在这里插入图片描述
1、13 记得算空格
2、studentteacher
3、I’m

例3.
在这里插入图片描述
(1)a b a a b c a c
    ~~~     0 1 1 2 2 3 1 2
(2)
利用改进的KMP()
未改进的KMP实在趟数太多了
nextval 0 1 0 2 1 3 0 2
第1趟
acabaabaabcacaabc
abaabcac
i=2 j=2
i=2 j=1
第2趟匹配
acabaabaabcacaabc
   ~~   abaabcac
i=3 j=1
第3趟匹配
acabaabaabcacaabc
   ~~    abaabcac
i=8 j=6
i=8 j=3
第4趟匹配
acabaabaabcacaabc
         ~~~~~~~~          abaabcac
匹配成功

例4.
在这里插入图片描述
表头(a,b,c,d)
表尾()

例5.
在这里插入图片描述
Tail(L) = ((orange,(strawberry,(banana)),peach),pear)
Head(Tail(L)) = (orange,(strawberry,(banana)),peach)
Tail(Head(Tail(L))) = ((strawberry,(banana)),peach)
Head(Tail(Head(Tail(L)))) = (strawberry,(banana))
Tail(Head(Tail(Head(Tail(L))))) = ((banana))
下一步只能用Head(),因为只有一个元素时,Tail取得的表尾是()
Head(Tail(Head(Tail(Head(Tail(L)))))) = (banana)
Head(Head(Tail(Head(Tail(Head(Tail(L))))))) = banana

五、树

树是一种非线性结构,在学习离散数学时就有了一定了解
树是n个结点的有限集合
空树 n = 0
非空树:1.有且仅有一个称之为根的结点
             ~~~~~~~~~~~~              2.除根结点以外的其余结点可分为m个互不相交有限集合,每个集合本身又是一棵树(子树)
基本术语看书上定义即可

5.1 二叉树

1.只有一个根
2.除根以外分为左子树和右子树
二叉树的子树有左右之分,次序不可以随意颠倒

5.11 性质

性质1:在二叉树的第i层上至多有2^(i-1)个结点(i>=1)
数学归纳法可以证明

性质2:深度为k的二叉树至多有2^(i-1)个结点(k>=1)

性质3:对任何一棵二叉树T,如果其叶子结点为n0,度为2的结点数为n2,则n0 = n2+1
证明:
设n1为二叉树T中度为1的结点数。
总结点数 n = n0 + n1 + n2
除了根结点,每个结点都有一个分支进入,设B为分支总数,
则 n = B+1, 同时这些分支由度为1和2的结点射出,所以B = n1 + n2

联立解得
                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                 n0 = n2 + 1(很重要!!!)

性质4:具有n个结点的完全二叉树的深度为log2n+1

性质5:如果对一棵有n个结点的完全二叉树(其深度为log2n+1)的结点按层序编号,则对任意结点i:
(1)如果i = 1,则结点i是二叉树的根,没有双亲,如果i>1, 其双亲是结点i/2
(2)如果2i > n, 则结点没有左孩子,否则左孩子是结点2i
(2)如果2i + 1 > n, 则结点没有右孩子,否则右孩子是结点2i + 1

5.12 存储方式

1.顺序存储
用一组地址连续的存储单元依次自上而下、自左向右存储完全二叉树
在这里插入图片描述
结点i
左孩子 2i
右孩子 2i+1
父结点 i/2
例如结点2的左孩子4右孩子5,父结点1

2.链式存储
用rchild和lchild两个指针指向左孩子和右孩子
含有n个结点的二叉链表,含有n个空链域
证明:
n个结点有2n个指针域
n个结点有n-1条边,即使用n-1个指针域
所以剩下的空链域为 2n -(n-1)= n + 1

5.12 遍历

二叉树有四种遍历方式:
(当用于存储表达式时对应三种表达式)
先序遍历       ~~~~~       根 左 右       ~~~~~      前缀表达式
中序遍历       ~~~~~       左 根 右       ~~~~~      中缀表达式(要加界限符)
后序遍历       ~~~~~       左 右 根       ~~~~~      后缀表达式
层次遍历       ~~~~~       根 左子树根 右子树根
两种实现方法(递归与非递归)
非递归显然时间复杂度低
非递归的思想即
在这里插入图片描述
每个结点会被访问三次
第一次访问
依次输出第一次被访问的结点(先序遍历)

第二次访问
依次输出第二次被访问的结点(中序遍历)

第三次访问
依次输出第三次被访问的结点(后序遍历)

后序遍历的具体代码可以看这遍blog后序遍历的四种非递归算法

#include<iostream>
#define OK 1
#define ERROR 0
using namespace std;
typedef int Status;
typedef char TElemType;

//------二叉树的二叉链表存储表示-------
typedef struct BiTNode
{
	TElemType data;
	struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
//-----链栈的存储结构------
typedef struct StackNode
{
	BiTree data;             //数据域为二叉链的指针类型
	struct StackNode *next;  //指针域
}StackNode,*LinkStack;
//----先序遍历建立二叉链表-----

void CreateBiTree(BiTree &T)
{
	char ch;
	cin>>ch;
	if(ch=='#') T=NULL;             //递归结束,建空树
	else
	{
		T=new BiTNode;              //生成根结点
		T->data=ch;                 //根结点数据域置为ch
		CreateBiTree(T->lchild);    //递归创建左子树
		CreateBiTree(T->rchild);    //递归创建右子树
	}
}
//------链栈初始化-------
Status InitStack(LinkStack &S)
{   //构造一个空栈 S 
	S=NULL;    //栈顶指针置空 
	return OK;
} 
//--------入栈----------
Status Push(LinkStack &S,BiTree e)
{   //在栈顶插入元素e 
    LinkStack p;
	p=new StackNode;  //生成新结点 
	p->data=e;        //将新结点数据域置为e 
	p->next=S;        //将新结点插入栈顶 
	S=p;              //修改栈顶指针为p 
	return OK;
} 
//------出栈--------
Status Pop(LinkStack &S,BiTree &e)
{   //删除栈顶元素,用e返回其值 
	if(S==NULL) return ERROR;  //栈空 
	e=S->data;                 //将栈顶元素赋给e 
	LinkStack p=S;             //用p临时保存栈顶元素空间,以备释放 
	S=S->next;                 //修改栈顶指针 
	delete p;                  //释放原栈顶元素空间 
	return OK; 
} 
//-----取栈顶元素------
BiTree GetTop(LinkStack &S)
{
	if(S!=NULL)
	   return S->data;         //不为空返回栈顶元素
}
//-----判断栈空------
Status StackEmpty(LinkStack &S)
{
	if(S==NULL) return OK;    //栈顶元素为空 
	return ERROR;
} 


//中序遍历的递归算法
void InOrderTraverse1(BiTree T) {
	if (T) {
		InOrderTraverse1(T->lchild);
		cout << T->data;
		InOrderTraverse1(T->rchild);
	}
}

//后序遍历的递归算法
void PostOrderTraverse(BiTree T) {
	if (T) {
		PostOrderTraverse(T->lchild);
		PostOrderTraverse(T->rchild);
		cout << T->data;
	}
}

//中序遍历的非递归算法
void InOrderTraverse2(BiTree T)
{ // 中序遍历二叉树T的非递归算法
	LinkStack S; BiTree p;
	BiTree q = new BiTNode;
	InitStack(S); p = T;
	while (p || !StackEmpty(S))
	{
		if (p) {
			Push(S, p);
			p = p->lchild;
		}
		else {
			Pop(S, q);
			cout << q->data;
			p = q->rchild;
		}
	}
} 

int main()
{
	BiTree tree;
	cout<<"请输入建立二叉链表的序列:"; //ABC##DE#G##F###
	CreateBiTree(tree);  //创建二叉树

	cout << "中序遍历递归算法的结果为:\n";
	InOrderTraverse1(tree); 
	cout << endl;
	
	cout << "中序遍历非递归算法的结果为:\n";
	InOrderTraverse2(tree); 
	cout << endl;

	cout<<"后序遍历递归算法的结果为:\n";
	PostOrderTraverse(tree); 
	cout << endl;
	system("pause");
    return 0; 
}

在这里插入图片描述
层序遍历使用队列就可以实现

5.2 线索二叉树

将二叉树线索化,线索化个人理解为即可以快速找前驱和后继
中序线索二叉树,即中序遍历后线索化
先序线索二叉树和后序线索二叉树同理
文字描述可能不太直观,可以看本章最后 应用题 例1(2)
至于代码实现,还没有空敲,有空敲完上传

5.3 树,森林

5.31存储结构

树的三种常用存储结构
1.双亲表示法(顺序存储)
每个结点增设一个伪指针,指示其双亲结点在数组中的位置,根结点下标为0,其伪指针域为-1 这种存储方式找双亲非常直接
2.孩子表示法(顺序存储+链式存储)这种存储方式找孩子非常直接
3.孩子兄弟表示法(二叉链表)
灵活,方便实现树转换为二叉树,缺点不方便查找双亲结点

5.32 树,森林,二叉树转换

1.森林与二叉树的转换(本质左兄弟,右孩子)
2.树与二叉树的转换(本质用孩子兄弟表示法存储树)

5.33 树,森林的遍历


深度优先
1.先根遍历(二叉树先序)
2.后根遍历(二叉树中序)
广度优先
层次遍历

森林
把森林转化成二叉树,对二叉树进行遍历
1.先序遍历
2.中序遍历

5.4 哈夫曼树,哈夫曼编码

哈夫曼树的构建
假设由n个结点
手算时个人比较喜欢
(1)将所有的结点从小到大排好n1,n2…
(2)将最小的两个n1+n2相加得到新的结点a1
(3)比较a1与剩下n-2个结点,如果a>n3,n4,则将n3与n4相加得到新的结点a2,否则将a1与n3相加得到新的结点a2,然后重复上述过程
具体见 应用题例2

哈夫曼编码就更简单了
(1)构建哈夫曼树
(2)给哈夫曼树的边设置0和1,左子树1,右子树0(或者左0右1)
设置的情况不同,得到的哈夫曼编码也就不同,但是他们的wpl是相同的,都是最优解
(3)从根节点到目标结点的0和1组成的数即为哈夫曼编码
具体见 应用题例3

5.5 例题

5.51 选择题

例1.把一棵树转换为二叉树后,这棵二叉树的形态是( A )
A.唯一的
B.有多种
C.有多种,但根结点都没有左孩子
D.有多种,但根结点都没有右孩子
孩子兄弟表示法

例2.一棵非空的二叉树的先序遍历序列与后序遍历序列正好相反,则该二叉树一定满足©。
A.所有的结点均无左孩子
B. 所有的结点均无右孩子
C.只有一个叶子结点
D.是任意一棵二叉树
先序:根左右
后序:左右根
显而易见没有左子树或者右子树(即只有一个叶子结点)

例3.设F是一个森林,B是由F变换得的二叉树。若F中有n个非终端结点,则B中右指针域为空的结点有( C) 个。
A.n-1
B. n
C.n+1
D.n+2
假设F有a个终端结点,总结点 = n + a
共有2(n+a)个指针域
根据 b = 总结点 - 1 = n + a - 1(b 边的数量就是使用了的指针域的数量)
空的指针域 = 2(n+a)-(n+a-1) = n + a + 1
二叉树B是由森林F变得的二叉树(左孩子右兄弟,n个非终端结点,即n个结点有孩子,即n个非空左指针域)
左空 = n + a - n = a
右空 = 空 - 左空 = n + a + 1 - a = n + 1

例4.已知一棵有2011个结点的树,其叶子结点个数为116,该树对应的二叉树中无右孩子的结点个数是(D)。
A. 115
B. 116
C. 1895
D. 1896
无右孩子分为两种情况一种是n0,一种是n1
已知n0=116,得n2 = n0 - 1 =115
所以n1 + n0 = n - n2 = 2011 - 115 =1896

5.52 应用题

例1.设一棵二叉树的先序、中序遍历序列为:先序遍历序列,ABDFCEGH、 中序遍历序列,BFDAGEHC。
求:
(1) 画出这棵二叉树;
(2) 画出这棵二叉树的后序线索树。
(3) 将这棵二叉树转换成对应的树(或森林)
在这里插入图片描述
2.已知下列字符A、B、C、D、E、F、G的权值分别为3、12、7、4、2、8,11,试填写出其对应哈夫曼树HT的存储结构的初态和终态。
在这里插入图片描述
3.假设用于通信的电文仅由 8 个字母组成,字母在电文中出现的频率分别为 0.07,0.19,0.02,0.06,0.32,0.03,0.21,0.10。
(1)试为这 8 个字母设计哈夫曼编码。
(2)试设计另一种由二进制表示的等长编码方案
(3)对于上述实例,比较两种方案的优缺点
在这里插入图片描述

六、图

图是一种非线性结构
图的几种类型
1、有向图(边有方向)
2、无向图(边没有方向)
3、简单图(不存在重复边和自环)
4、多重图
5、完全图
6、子图
7、连通
8、连通图、连通分量
9、生成树、生成森林
10、顶点的度、入度、出度
11、边的权和网
12、稠密图、稀疏图
13、路径、路径长度、回路
14、简单路径、简单回路
15、距离
16、有向树

6.1 图的存储

6.11邻接矩阵

数组存储

#include <iostream>
using namespace std;
#define MaxInt 32767 //表示极大值,即∞
#define MVNum 100 //最大顶点数
#define OK 1
typedef char VerTexType; //假设顶点的数据类型为字符型
typedef int ArcType; //假设边的权值类型为整型

//图的邻接矩阵存储表示
typedef struct{
VerTexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum,arcnum; //图的当前点数和边数
}AMGraph;

int LocateVex(AMGraph G , VerTexType v){//确定点v在G中的位置
	for(int i = 0; i < G.vexnum; ++i)
		if(G.vexs[i] == v)	
			return i;
	return -1;
}

int CreateUDN(AMGraph &G){ //采用邻接矩阵表示法,创建无向网G
	int i , j , k;
	cout <<"请输入总顶点数,总边数,以空格隔开:";
	cin >> G.vexnum >> G.arcnum; //输入总顶点数,总边数
	cout << endl;
	cout << "输入点的名称,如a" << endl;
	for(i = 0; i < G.vexnum; ++i){
	cout << "请输入第" << (i+1) << "个点的名称:";
	cin >> G.vexs[i]; //依次输入点的信息
	}
	cout << endl;
	for(i = 0; i < G.vexnum; ++i) //初始化邻接矩阵,边的权值均置为极大值MaxInt
	for(j = 0; j < G.vexnum; ++j)
	G.arcs[i][j] = MaxInt;
	cout << "输入边依附的顶点及权值,如 a b 5" << endl;
	for(k = 0; k < G.arcnum;++k){ //构造邻接矩阵
	VerTexType v1 , v2;
	ArcType w;
	cout << "请输入第" << (k + 1) << "条边依附的顶点及权值:";
	cin >> v1 >> v2 >> w; //输入一条边依附的顶点及权值
	i = LocateVex(G, v1);
	j = LocateVex(G, v2); //确定v1和v2在G中的位置,即顶点数组的下标
	G.arcs[i][j] = w; //边<v1, v2>的权值置为w
	G.arcs[j][i] = G.arcs[i][j]; //置<v1, v2>的对称边<v2, v1>的权值为w
	}
	return OK;
}

int main(){
	cout << "************采用邻接矩阵表示法创建无向网**************" << endl << endl;	
	AMGraph G;
	int i , j;
	CreateUDN(G);
	cout <<endl;
	cout << "*****邻接矩阵表示法创建的无向网*****" << endl;
	for(i = 0 ; i < G.vexnum ; ++i){
		for(j = 0; j < G.vexnum; ++j){
			if(j != G.vexnum - 1){
				if(G.arcs[i][j] != MaxInt)
					cout << G.arcs[i][j] << "\t";
				else
					cout << "∞" << "\t";
			}
			else{
				if(G.arcs[i][j] != MaxInt)
					cout << G.arcs[i][j] <<endl;
				else
					cout << "∞" <<endl;
			}
		}
	}//for
	cout <<endl;
	system("pause");
	return 0;
}

在这里插入图片描述

6.12邻接表

链表存储

#include <iostream>
using namespace std;
#define MVNum 100 //最大顶点数
#define OK 1
typedef char VerTexType; //顶点信息
typedef int OtherInfo; //和边相关的信息

//- - - - -图的邻接表存储表示- - - - -
typedef struct ArcNode{ //边结点
	int adjvex; //该边所指向的顶点的位置
	struct ArcNode *nextarc; //指向下一条边的指针
	OtherInfo info; //和边相关的信息
}ArcNode;

typedef struct VNode{
	VerTexType data; //顶点信息
	ArcNode *firstarc; //指向第一条依附该顶点的边的指针
}VNode, AdjList[MVNum]; //AdjList表示邻接表类型

typedef struct{
	AdjList vertices; //邻接表
	int vexnum, arcnum; //图的当前顶点数和边数
}ALGraph;

int LocateVex(ALGraph G , VerTexType v){
//确定点v在G中的位置
	for(int i = 0; i < G.vexnum; ++i)
		if(G.vertices[i].data == v)
			return i;
return -1;
}//LocateVex

int CreateUDG(ALGraph &G){
//采用邻接表表示法,创建无向图G
	int i , k;
	cout <<"请输入总顶点数,总边数中间以空格隔开:";
	cin >> G.vexnum >> G.arcnum; //输入总顶点数,总边数
	cout << endl;
	cout << "输入点的名称,如 a " <<endl;
	for(i = 0; i < G.vexnum; ++i){ //输入各点,构造表头结点表
		cout << "请输入第" << (i+1) << "个点的名称:";
	cin >> G.vertices[i].data; //输入顶点值
	G.vertices[i].firstarc=NULL; //初始化表头结点的指针域为NULL
}
cout << endl;
cout << "请输入一条边依附的顶点,如 a b" << endl;

for(k = 0; k < G.arcnum;++k){ //输入各边,构造邻接表
	VerTexType v1 , v2;
	int i , j;
	cout << "请输入第" << (k + 1) << "条边依附的顶点:";
	cin >> v1 >> v2; //输入一条边依附的两个顶点
	i = LocateVex(G, v1); j = LocateVex(G, v2);
//确定v1和v2在G中位置,即顶点在G.vertices中的序号
	ArcNode *p1=new ArcNode; //生成一个新的边结点*p1
	p1->adjvex=j; //邻接点序号为j
	p1->nextarc= G.vertices[i].firstarc; G.vertices[i].firstarc=p1;
//将新结点*p1插入顶点vi的边表头部
	ArcNode *p2=new ArcNode; //生成另一个对称的新的边结点*p2
	p2->adjvex=i; //邻接点序号为i
	p2->nextarc= G.vertices[j].firstarc; G.vertices[j].firstarc=p2;
//将新结点*p2插入顶点vj的边表头部
}//for
	return OK;
}//CreateUDG

int main(){
	cout << "************采用邻接表表示法创建无向图**************" << endl << endl;
	ALGraph G;
	CreateUDG(G);
	int i;
	cout << endl;
	cout << "*****邻接表表示法创建的无向图*****" << endl;
	for(i = 0 ; i < G.vexnum ; ++i){
		VNode temp = G.vertices[i];
		ArcNode *p = temp.firstarc;
		if(p == NULL){
			cout << G.vertices[i].data;
			cout << endl;
		}
		else{
			cout << temp.data;
			while(p){
				cout << "->";
				cout << p->adjvex;
				p = p->nextarc;
			}
		}
		cout << endl;
	}
	system("pause");
	return 0;
}

在这里插入图片描述

6.13 十字链表

6.14 邻接多重表

6.2 图的遍历

6.22 广度优先搜索 BFS

1、求解单源点最短路径
2、广度优先生成树

6.23 深度优先搜索 DFS

深度优先生成树和生成森林

6.3 图的应用

6.31 最小生成树

Prim 加点法 依次找权值最小的边,将点加入集合,直到点集等于全集
Kruskal 加边法 与Prim思想一样,只不过将边加入集合,直到边集等于全集
Kruskal更适合求稀疏网的最小生成树
例题见应用题第二题

6.32 拓扑排序

对有向无环图的一种排序
实现方法:
1.从AOV网中选择一个没有前驱的顶点输出
2.从网中删除该顶点和所有以它为起点的有向边
3.重复1和2直到AOV网为空
例题见应用题第三题

6.33 最短路径问题

Dijkstra和Floyd
具体见应用题例题

6.4 例题

6.41 选择题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
第8题的五个序列为
vo v1 v3 v2
vo v2 v3 v1
vo v2 v1 v3
vo v3 v1 v2
vo v3 v2 v1
因为是深度优先遍历,必须走到底,没有下一个邻接点才返回,所以v1和v3中间不能有v2,即不存在vo v1 v2 v3 v4
在这里插入图片描述
第9题
广度优先遍历,从邻接表可以看出,vo的邻接点有1,2,3,依次遍历结束
序列为0 1 2 3
深度优先遍历,从0开始走到底,vo的邻接点有v1,v1的邻接点有v2,v2的邻接点有v3
序列为0 1 2 3
第10题
拓扑排序,通过反复选择无前驱的顶点完成,若有环则无法找到一个无前去的顶点

6.42 应用题

1在这里插入图片描述在这里插入图片描述

2.在这里插入图片描述
在这里插入图片描述
一开始权值最小的边为fg。 f,g点确定
接下来权值为3,ac,ef。a,c,e点确定
权值为4,ab, dh。b,h点确定
权值为5的此时有4条,但只需要bd,dg即可构成最小生成树,不需要cd的原因是会构成回路,最小生成树不能有回路

3.在这里插入图片描述
图一的拓扑序列为v1 v2 v3 v4 v5 v6 v7
答案不唯一,只要满足该节点没有前驱即可,如 v1 v3 v2 v4 v5 v6也可以
图二不能输出拓扑序列,因为v4,v5,v6构成回路
4.在这里插入图片描述
在这里插入图片描述

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值