数据结构笔记
导论
- 本笔记由郝斌老师的视频而写出
总结
- 数据结构
- 狭义的讲:
- 数据存储是专门研究数据存储的问题
- 数据结构=个体的存储+个体关系的存储
- 广义的讲
- 数据结构即包含数据的存储也包含数据的操作
- 对存储数据的操作就是算法
- 狭义的讲:
- 算法:
- 狭义的讲
- 算法是和数据的存储方式密切相关
- 算法=对存储数据的操作
- 广义的讲
- 算法和数据的存储方式无关
- 这就是泛型思想
- 狭义的讲
- 数据的存储结构(常见的几种)
- 线性
- 连续存储【数组】
- 优点
- 存取(读取)速度很快
- 缺点
- 事先必须知道数组的长度
- 插入删除元素很慢
- 空间通常是有闲置
- 需要大块连续的内存块
- 优点
- 离散存储【链表】
- 优点
- 空间没有限制
- 插入删除元素很快
- 缺点
- 存取速度很慢
- 链表总的说有三种
- 单链表(尾指针指向后一节点)
- 双链表(除头/尾节点,其中的每个节点的头/尾指针都指向了前/后个节点,注意其尾节点不指向头节点,头节点也不指向尾节点)
- 循环链表(与单链表类似,但最后一个节点的尾指向头节点)
- 双循环链表(融合了双链表和循环链表,每个节点都的头/尾指针都指向了前/后个节点,包括头尾节点)
- 优点
- 线性结构的应用——栈
- 定义:
- 一种进而与实现“先进后出”的数据存储结构
- 分类
- 静态栈
- 动态栈(常用)
- 从实现来说就是数组与链表,从内存莱说就是连续与随机
- 定义:
- 线性结构的应用——队列
- 递归
- 定义
- 一个函数自己直接或间接调用自己
- 递归和循环的优缺点
- 递归:
- 易于理解
- 速度慢
- 存储空间大
- 循环
- 不易理解
- 速度快
- 存储空间小
- 递归:
- 定义
- 连续存储【数组】
- 非线性
- 树
- 树定义
- 有且只有一个称为根的节点
- 有若干个互不相交的子树,这些子树本身也是一棵树
- 树是由节点和边组成
- 每个节点只有一个父节点,但可以有多个子节点
- 术语
- 树分类
- 一般树
- 二叉树
- 任意一个子节点的个数最多两个,且子节点的左右位置不可更改
- 二叉树的分类
- 一般二叉树
- 满二叉树
- 完全二叉树
- 完全二叉树是路径最短的二叉树,但路径最短的二叉树不一定是完全二叉树
- 森林
- 树的存储
- 二叉树的存储
- 连续存储(完全二叉树)
- 查找节点的父节点和子节点速度很快,但耗用内存空间过大
- 链式存储
- 连续存储(完全二叉树)
- 一般树的存储
- 双亲表示法
- 孩子表示法
- 双亲孩子表示法
- 二叉树表示法
- 左指针域指向它的第一个孩子
- 右指针域指向它的堂兄弟
- 森林的存储
- 二叉树的存储
- 树的操作
- 树的遍历
- 先序遍历
- 先访问根节点
- 再先序访问左子树
- 再先序访问右子树
- 中序遍历
- 中序遍历左子树
- 再访问根节点
- 再中序遍历右子树
- 后序遍历
- 后序遍历左子树
- 再后序遍历右子树
- 再访问根节点
- 先序遍历
- 树的遍历
- 已知两种遍历求原始二叉树
- 已知先序和中序求后序
- 已知中序和后序求先序
- extra
- 知道先序和后序求二叉树虽然很难,但通过枚举还是可以实现
- 树定义
- 图
- 树
- 线性
- 逻辑结构
- 线性(线性结构都比较成熟)
- 数组
- 链表
- 栈和队列是一种特殊的线性结构
- 栈和队列都是线性结构,也是一种逻辑结构,他们既可以用顺序存储(顺序栈,顺序队列),也可以链式存储(链栈和链式队列)
- 栈只允许在栈头操作与删除
- 队列允许在一端删除在另一端插入
- 非线性(非线性结构还在发展)
- 线性(线性结构都比较成熟)
- 后结
- 什么是数据结构?
- 数据结构研究的是数据的存储和数据的操作的一门学问
- 数据的存储分两部分
- 个体的存储
- 个体关系的存储
- 从某个角度而言,数据的存储最核心的时个体关系的存储,个体的存储可以忽略不记
- 什么是泛型
- 同一种逻辑结构,无论改逻辑结构物理存储时什么样子的,我们都可以对他执行相同的操作
- 什么是数据结构?
衡量算法的标准
- 时间复杂度
- 大概程序要执行的次数,而非执行的时间
- 空间复杂度
- 算法执行过程中大概所占用的最大内存
- 难易程度
- 健壮性
线性结构
连续存储 [数组]
- 参数需求
- 首地址
- 长度
- 有效的个数
实现
数组和指针可以说是数据结构的基石,也可以说是链表、队列、栈的基础了
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
//#include <stdbool.h>
// 要么引入stdbool.h要么自己定义bool两种方法
// 以下的define由于gcc不支持bool所以要自己定义bool的值
#define bool char
#define true 1
#define false 0
// bool定义结束
struct Arr
{
int * pBase; //存储的时数组第一个元素的地址
int len; //数组所能容纳的最大元素的个数
int cnt; //当前数组有效元素的个数
};
void init_arr(struct Arr *arr, int length);
bool append_arr(struct Arr * pArr,int val);//追加
bool insert_arr(struct Arr * pArr,int pos,int val);
bool delete_arr(struct Arr * pArr,int pos,int * pVal);
bool is_empty(struct Arr *pArr);
bool is_full(struct Arr * pArr);
void sort_arr(struct Arr * pArr);
void show_arr(struct Arr *pArr);
void inversion_arr(struct Arr * pArr); //倒置
int main()
{
struct Arr arr;
int val;
init_arr(&arr, 6);
show_arr(&arr);
append_arr(&arr, 1);
append_arr(&arr, 2);
append_arr(&arr, 3);
append_arr(&arr, 4);
append_arr(&arr, 5);
append_arr(&arr, 6);
if (delete_arr(&arr, 5,&val))
{
printf("delete success! You delete num is %d\n",val);
}
else
printf("delete false!\n");
show_arr(&arr);
insert_arr(&arr,3,-5);
show_arr(&arr);
printf("inversion the list is:\n");
inversion_arr(&arr);
show_arr(&arr);
printf("sort the list is:\n");
sort_arr(&arr);
show_arr(&arr);
return 0;
}
void init_arr(struct Arr *pArr, int length)
{
pArr->pBase =(int *)malloc(sizeof(int) * length);
if(NULL==pArr->pBase)
{
printf("create pBase false!\n");
exit(-1);
}
else
{
pArr->len = length;
pArr->cnt = 0;
}
}
bool is_empty(struct Arr *pArr)
{
if (pArr->cnt==0)
return true;
else
return false;
}
bool is_full(struct Arr *pArr)
{
if(pArr->cnt ==pArr->len)
return true;
else
return false;
}
void show_arr(struct Arr *pArr)
{
if (is_empty(pArr))
{
printf("arr is empty!\n");
}
else
{
for(int i=0;i<pArr->cnt;++i)
printf("%d ",pArr->pBase[i]);
printf("\n");
}
return;
}
bool append_arr(struct Arr *pArr, int val)
{
if (is_full(pArr))
return false;
pArr->pBase[pArr->cnt]=val;
(pArr->cnt) ++;
return true;
}
bool insert_arr(struct Arr *pArr, int pos, int val)
{
if(is_full(pArr))
return false;
if(pos<1 || pos>pArr->cnt+1)
return false;
for(int i=pArr->cnt-1;i<pos-1;--i)
{
pArr->pBase[i+1]=pArr->pBase[i];
}
pArr->pBase[pos-1] = val;
return true;
}
bool delete_arr(struct Arr *pArr, int pos, int *pVal)
{
if(is_empty(pArr))
return false;
if(pos<1||pos>pArr->cnt)
return false;
*pVal = pArr->pBase[pos-1];
for(int i=pos;i<pArr->cnt;++i){
pArr->pBase[i-1]=pArr->pBase[i];
}
(pArr->cnt)--;
return true;
}
void inversion_arr(struct Arr *pArr)
{ //倒置整个数组
int i=0;
int j=pArr->cnt-1;
int n;
while (i<j)
{
n=pArr->pBase[i];
pArr->pBase[i]=pArr->pBase[j];
pArr->pBase[j]=n;
++i;--j;
}
}
void sort_arr(struct Arr *pArr){
int n;
for(int i=0;i<pArr->cnt;++i)
{
for(int j=i+1;j<pArr->cnt;++j)
{
if (pArr->pBase[i] > pArr->pBase[j])
{
n=pArr->pBase[i];
pArr->pBase[i] = pArr->pBase[j];
pArr->pBase[j]=n;
}
}
}
}
离散存储 [链表]
定义
-
n个节点离散分配
-
彼此通过指针相连
-
每个节点只有一个前驱节点,每个节点只有一个后续节点
-
首节点没有前驱节点,尾节点没有后续节点
-
专业术语
- 首节点
- 第一个有效节点
- 尾节点
- 最后一个有效节点
- 尾节点也可以看成NULL
- 头节点
- 头节点的数据类型和首节点类型一样
- 第一个有效节点的前一个节点
- 头节点并不存放有效数据
- 加头节点的目的是为了方便我们对列表的操作(增删改查)
- 头指针
- 指向头节点的指针变量
- 尾指针
- 指向尾节点的指针
- 首节点
-
参数需求(如果要通过一个函数来对链表进行处理,我们至少需要接受链表的那些参数
- 头指针
- 因为我们通过一个头指针就可以推算出其他的所有信息
- 头指针
分类
-
单链表
- $A\Rightarrow B\Rightarrow C(\Rightarrow NULL)$
-
双链表
- 每一个节点有两个指针域
- $A\Leftrightarrow B\Leftrightarrow C(\Rightarrow NULL)$
-
循环链表
- 能通过任何一个节点找到其他所有的节点
- $A\Rightarrow B\Rightarrow B\Rightarrow C(\Rightarrow A)$
-
非循环链表
-
结构体定义
typedef struct Node { int data; // 数据域 struct Node * pNext; //指针域 }NODE, *PNODE; //NODE等价于struct Node //PNODE等价于struct Node *
实现原理
-
遍历
-
查找
-
清空
-
销毁
-
求长度
-
排序
-
删除节点
// 注意这是伪算法,并没有真正的实现 r = p->pNext;//p->pNext表示p所指向结构体变量中pNExt成员本身 p->pNext = p->pNext->pNext free r; //删除r指向节点所占的内存,不是删除r本身所占内存 // ----错误示例----START /*注意不能单纯这样这样写,因为这样会导致内存泄露! 由于p->pNext变量已经改变所以指向 中间值的地址已经没有了,但其在内存中还是存在的*/ p->pNext = p->pNext->pNext // ----错误示例----END
-
插入节点
// 注意这是伪算法,并没有真正的实现 // pNext表示指向下一节点 // 假设有链表A->B->C // 先有D要插入到AB之间 // 方法1【推荐】 D->pNext = A->pNext; A->pNext = D; // 方法2,与方法1一样 r = A->pNext; A->pNext = D; D->pNext = r;
-
算法
- 狭义的算法是与数据的存储方式密切相关
- 广义的算法是与数据的存储方式无关
- 泛型
- 利用某种技术达到的效果就是:不同的存储方式,执行的操作时是一样的
-
链表的优缺点(见下面总结)
-
链表总的说有三种
- 单链表(尾指针指向后一节点)
- 双链表(除头/尾节点,其中的每个节点的头/尾指针都指向了前/后个节点,注意其尾节点不指向头节点,头节点也不指向尾节点)
- 循环链表(与单链表类似,但最后一个节点的尾指向头节点)
- 双循环链表(融合了双链表和循环链表,每个节点都的头/尾指针都指向了前/后个节点,包括头尾节点)
实现代码
- 视频上只演示了单链表的方法,所以这里只有单链表的代码,然后指针真的很重要,记得要学习好
- 这里还涉及到一个
typedef
,这个可以理解为自定义类型 - 比如说
typedef struct Node{···} NODE, *PNODE
- NODE其实就是
struct Node{···}
- *PNODE其实就是
struct Node{···} *
- 本来定义一个结构体Node变量Q需要
struct Node Q;
- 现在只需要(很方便呢😆
NODE Q
- NODE其实就是
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#define bool char
#define true 1
#define false 0
typedef struct Node
{
int data; // 数据域
struct Node * pNext; //指针域
}NODE, *PNODE; //NODE等价于struct Node PNODE等价于struct Node *
//函数声明
//创建链表
PNODE create_list(void);
//遍历并输出链表的data
void traverse_list(PNODE pHead);
//判断链表是否为空
bool is_empty(PNODE pHead);
//求链表长度
int length_list(PNODE);
//在pHead所指向链表的第pos个节点的前面插入一个新的节点,该节点的值是val,pos的值是从1开始
bool insert_list(PNODE pHead, int pos, int val);
//删除链表第pos个节点,并将删除的节点的值存入pVal所指向的变量中,pos的值是从1开始
bool delete_list(PNODE pHead, int pos, int *pVal);
//对链表进行排序
void sort_list(PNODE);
int main(void)
{
PNODE pHead = NULL; //等价于 struct Node * pHead=NULL;
int val;
//create_list()功能是创建一个非循环单练表,并将量表的头节点的地址赋予给pHead
pHead = create_list();
insert_list(pHead, 2, 33);
sort_list(pHead);
traverse_list(pHead);
if(delete_list(pHead,4,&val))
{
printf("delete success,del num is %d\n",val);
}
else
{
printf("delete faled! choice position unexist!\n");
}
traverse_list(pHead);
return 0;
}
//初始化链表
PNODE create_list(void)
{
int len; //存放有效节点的个数
int val; //临时存放用户输入的节点的值
int i;
PNODE pHead = (PNODE)malloc(sizeof(NODE));
if(NULL == pHead)
{
printf("create false!\n");
exit(-1);
}
//创建一个尾节点,将头节点的尾指针指向NULL,当len为1时,即指向NULL
PNODE pTail = pHead;
pTail->pNext=NULL;
printf("please input list length:");
scanf("%d",&len);
for(i=0;i<len;++i)
{
printf("please input the %d value:",i+1);
scanf("%d",&val);
PNODE pNew=(PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("create false!\n");
exit(-1);
}
//若len>1则指向循环,创建一个pNew作为新的节点
//【第1次】将val存入pNew的data,并将pTail代表的pHead的尾指针指向pNew,并将pNew的尾指针设为NULL,然后再将pTail指向pNew
pNew->data=val;
pTail->pNext=pNew;
pNew->pNext=NULL;
pTail=pNew;
}
// 循环执行完毕就完成了A->B->C···,然后将头指针pHead作为函数返回即可
return pHead;
}
//遍历链表,并输出链表中所有的值,用p来relay指向头节点的下一节点然后循环[将data域输出; 再指向下一节点]
void traverse_list(PNODE pHead)
{
PNODE p=pHead->pNext;
while(NULL!=p)
{
printf("%d ",p->data);
p=p->pNext;
}
printf("\n");
}
//判断头节点的下一节点是否为空即可判断链表是否为空,注意链表是不存在满的情况,因为链表可以有无限个,但是内存会有满的时候!
bool is_empty(PNODE pHead)
{
if(NULL==pHead->pNext)
return true;
else
return false;
}
//计算链表长度,并返回整数的长度值
int length_list(PNODE pHead)
{
PNODE p=pHead->pNext;
int len=0;
while(NULL!=p)
{
++len;
p=p->pNext;
}
return len;
}
//对指针的data域数据进行比较,然后将data域互换排序
void sort_list(PNODE pHead)
{
int i,j,k;
int len=length_list(pHead);
PNODE p,q;
//for循环来循环需要排序的次数,并在其中嵌套指向下一个链表的逻辑
for(i=0,p=pHead->pNext;i<len-1;i++,p=p->pNext)
{
for(j=i+1,q=p->pNext;j<len;++j,q=q->pNext)
{
if(p->data>q->data)
{
//创建relay接收p的data
k=p->data;
//将p的data与q的data互换
p->data=q->data;
//将q的data改为relay的值
q->data=k;
}
}
}
}
//插入链表,需要先
bool insert_list(PNODE pHead, int pos, int val){
//用于判断链表是否为空、参数是否有误,
int i=0;
PNODE p=pHead;
//因为插入时p的位置要在pos的前面,所以判断p即可
while(NULL!=p&&i<pos-1)
{
p=p->pNext;
++i;
}
//若输入有误,pos-1会<i
//若链表为空则p会尾NULL
if (i>pos-1||NULL==p)
return false;
//执行到这里的时候,p已经指向第pos-1个节点,pos-1是否存在无所谓
//若输入无误,pos-1会>i而且p会指向到pos-1的位置,即需要插入的位置
//新建节点作relay
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if(NULL==pNew){
printf("create false\n");
exit(-1);
}
//A->B->C,要插入D到bc之间,先将数据放入D,然后将B指向D,D指向C,即A->B->D->C
//作插入的新节点先放入数据
pNew->data=val;
//新建q节点指向插入节点的后一个节点
PNODE q=p->pNext;
//将插入节点的前一个节点的尾节点指向新节点
p->pNext=pNew;
//再将新节点指向插入节点的后一个节点
pNew->pNext=q;
return true;
}
bool delete_list(PNODE pHead, int pos, int *pVal)
{
int i=0;
PNODE p=pHead;
//因为删除操作是删除pos位置的值,所以判断p->pNext
while(NULL!=p->pNext&&i<pos-1)
{
p=p->pNext;
++i;
}
if(i>pos-1||NULL==p->pNext)
return false;
// 执行到这里的时候说明p的值已经指向第pos-1个节点,并且pos-1存在
//ABCD,要删除C,此时p指向B,新建q指向C以free,将C的data存放以输出,将B指向D,然后free掉c,并将C=NULL(防止复活)
//p指向要删除的节点
PNODE q=p->pNext;
//将pVal等于要删除的链表的数据
*pVal=q->data;
//将p指向要删除节点的下一个节点
p->pNext=p->pNext->pNext;
//释放内存
free(q);
q=NULL;
return true;
}
栈 - 线性结构的两种常用应用
定义
- 一种进而与实现“先进后出”的数据存储结构
- 栈类似于箱子
分类
- 静态栈
- 动态栈(常用)
算法
- 出栈(删除一个节点)
- pop,pTop
- 出栈则将最上面的节点删除并将pTop指向下一个节点
- 压栈(添加一个节点)
- push,pTop
- 压栈则添加一个节点,并将pTop指向新的节点
实现
- 栈是“先进后出”
- 栈按内存有静态栈和动态栈(常用),从实现来说就是数组与链表,从内存莱说就是连续与随机
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#define bool char
#define true 1
#define false 0
typedef struct node{
int data;
struct node * pNext;
}NODE, *PNODE;
typedef struct stack{
PNODE pTop;
PNODE pBottom;
}STACK,* PSTACK;
// 函数初始化
// 函数的元素声明可以不写函数名只写类型
// 初始化栈
void init(PSTACK);
// 入栈,将int元素入栈
void push(PSTACK,int);
// 判断栈是否为空,注意栈的空间没有限制
bool empty(PSTACK);
// 遍历,将栈从pTOP至pBottom遍历并输出
bool traverse(PSTACK);
// 出栈,由于只将最上面的元素输出并清除,所以不需要position
bool pop(PSTACK,int *);
// 清除,将栈内的所以PNODE都清除并free掉
void clear(PSTACK);
int main(){
STACK S;
int val;
init(&S);
push(&S, 1);
push(&S, 2);
push(&S, 3);
push(&S, 4);
traverse(&S);
//出栈,val用来接收出栈的值
if (pop(&S, &val))
printf("stack output is %d\n",val);
else
printf("stack output faled!\n");
traverse(&S);
//清除
clear(&S);
traverse(&S);
}
void init(PSTACK pS)
{
pS->pTop=(PNODE)malloc(sizeof(NODE));
if (pS->pTop==NULL){
printf("malloc create faled!\n");
exit(-1);
}
// 将pBottom和pTop都指向一个空的PNODE
pS->pBottom = pS->pTop;
// 将头指针设为NULL
pS->pTop->pNext = NULL; //也可以pS->pTop->pBottom=NULL;
}
void push(PSTACK pS, int val)
{ // 将新的pNew的data赋值并将pTop指向pNew
PNODE pNew=(PNODE)malloc(sizeof(NODE));
if (pNew==NULL)
{
printf("malloc crate faled!\n");
exit(-1);
}
pNew->data = val;
pNew->pNext = pS->pTop;
pS->pTop = pNew;
}
bool empty(PSTACK pS)
{
if (pS->pTop==pS->pBottom)
return true;
else
return false;
}
bool traverse(PSTACK pS)
{// 将p作为临时PNODE,当pNext不为NULL则输出data并指向下一指针
if(empty(pS))
{
printf("stack is empty!\n");
return false;
}
else
{
PNODE p=pS->pTop;
while(p->pNext!=NULL)
{
printf("%d ",p->data);
p=p->pNext;
}
printf("\n");
}
}
bool pop(PSTACK pS,int * val)
{// 将p作为临时PNODE,将pTop给p,然后将pTop指向下一PNODE,再将p的data放入val,最后清空p
if (empty(pS))
{
printf("stack is empty!\n");
return false;
}
else
{
PNODE p=pS->pTop;
pS->pTop = pS->pTop->pNext;
*val = p->data;
// 先将内存地址free掉,再将指针设为NULL避免出现问题
free(p);
p=NULL;
}
}
void clear(PSTACK pS)
{ //将栈内所有的PNODE清除,并将pTop指向pBottom即NULL
if (empty(pS)){
return;
}
else
{
PNODE p = pS->pTop;
PNODE q=NULL;
while(p!=pS->pBottom)
{
q=p->pNext;
free(p);
p=q;
}
pS->pTop=pS->pBottom;
}
}
队列 - 线性结构的两种常用应用
定义:
- 一种可以实现“先进先出”的存储结构
分类
- 链式队列
- 用链表实现
- 静态队列
- 用数组实现
- 静态队列通常都必须是循环队列
-
静态队列为什么都是循环队列
- 如果不使用循环队列,删除元素的就会造成浪费,前面的元素会用不了
-
循环队列需要几个参数来确定
- 两个参数
- front(队头)
- rear(队尾)
- 存放元素的数组
- 两个参数
-
循环队列各个参数的含义
- 两个参数再不同场合有不同的含义,可以先这样记
- 队列初始化
- front和rear的值都是零
- 队列非空
- front代表的是队列的第一个元素
- rear代表的是队列的最后一个有效雨啊素的下一个元素
- 队列空
- front和rear的值相等,但不一定是零
- 队列初始化
- 两个参数再不同场合有不同的含义,可以先这样记
-
循环队列入队伪算法
-
两步完成
- 将值存入r所指向的位置
- rear=(rear+1)%数组的长度
- rear+1是因为循环队列正在入队(添加)所以需要+1
- 注意这里所用的取余方式
- 1%3=1
- 2%3=2
- 3%3=0
- n-1%n=n
-
-
循环队列出队伪算法
- front=(front+1)%数组的长度
-
如何判断循环队列是否为空
- 如果front与rear的值相等,则该队列就一定为空
-
如何判断循环队列是否已满
- 由于front与rear在队列中的位置不确定,可大可小可相等,所以不能用比大小的方式判断
- 两种方式
- 多增加一个标表志参数
- 独立设置一个标志,队列满为1,不满则为0
- 少用一个元素(常用)
-
如果rear和front的值紧挨着,则队列已满
-
用C语言伪算法表示就是
if ((r+1)%数组长度 == f) 已满 else 不满
- 设定空出一个节点,当队列满时,rear则指向空的那个节点,当
(r+1)%数组长度
时,即等于front的位置
- 设定空出一个节点,当队列满时,rear则指向空的那个节点,当
-
- 多增加一个标表志参数
-
算法
- 出队(删除一个节点)
- front
- front永远指向队列的头,出队则将front向上移
- front
- 入队(添加一个节点)
- rear
- rear永远指向当前队列的下一个位置,入队则将rear向上移
- rear
实现
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
// #include <stdbool.h>
#define bool char
#define true 1
#define false 0
typedef struct Queue
{
int *pBase;
int front;
int rear;
}QUEUE;
void init(QUEUE *);
// 入队
bool en_queue(QUEUE *,int);
// 遍历
void traverse_queue(QUEUE *);
bool full_queue(QUEUE *);
// 出队
bool out_queue(QUEUE *,int *);
bool empty_queue(QUEUE *);
int main()
{
QUEUE Q;
int val;
init(&Q);
en_queue(&Q, 1);
en_queue(&Q, 2);
en_queue(&Q, 3);
en_queue(&Q, 4);
en_queue(&Q, 5);
en_queue(&Q, 6);
traverse_queue(&Q);
if (out_queue(&Q, &val))
printf("out queue success, value is:%d\n",val);
else
{
printf("out queue faled!\n");
exit(-1);
}
traverse_queue(&Q);
return 0;
}
void init(QUEUE *pQ)
{// 初始化6个空间大小的整数型数组,front和rear初始都为0
pQ->pBase=(int*)malloc(sizeof(QUEUE)*6);
pQ->front=0;
pQ->rear=0;
}
bool full_queue(QUEUE *pQ)
{
if ((pQ->rear+1)%6==pQ->front)
return true;
else
return false;
}
bool en_queue(QUEUE *pQ,int val)
{// 入队,若队满则false,否则先将val存入下标为rear的pBase,再将(rear+1)%6指向下一元素
if(full_queue(pQ))
{
return false;
}
else
{
// (rear+1)%6,若rear+1<6则为其原本+1的数,否则rear+1=6则6%6=0
pQ->pBase[pQ->rear]=val;
pQ->rear = (pQ->rear + 1)%6;
return true;
}
}
void traverse_queue(QUEUE *pQ)
{// 遍历队列,用i获取队头front的下标,当i的下标不等于队尾的下标则一直循环输出pBase内的值
int i=pQ->front;
while(i!=pQ->rear)
{
printf("%d ",pQ->pBase[i]);
i = (i + 1)%6;
}
printf("\n");
}
bool empty_queue(QUEUE*pQ)
{
if(pQ->rear==pQ->front)
return true;
else
return false;
}
bool out_queue(QUEUE * pQ, int *pVal)
{
if(empty_queue(pQ))
return false;
else
{
*pVal = pQ->pBase[pQ->front];
pQ->front = (pQ->front + 1)%6;
return true;
}
}
-
分析
-
循环队列
- 当前情况:front指向
4
,rear指向5
,此时这个队列只有一个元素A
,因为rear是指向当前队列的下一个位置
-
入队:若此时入队的话,'中'字进入队列,则会将rear指向
0
,5
的位置会变成中,此时这个循环队列有两个元素 -
出队:若此时出队的话,front会向上移,此时这个循环队列有一个元素
- 当前情况:front指向
-
-
队列的具体应用
- 所有和时间有关的操作都与队列有关
递归
定义
-
一个函数自己直接或间接调用自己
-
递归要满足的三个条件
- 递归必须得有一个明确的中止条件
- 该函数所处理的数据规模必须在递减
- 一般递归是在递减的,但递归的值也可以递增,但处理的规模实际上是在递减的
- 这个转化必须是可解的
-
循环和递归
- 一般来说所有的递归都可以用递归实现,但递归不一定可以用循环来实现
- 递归和循环的优缺点
- 递归:
- 易于理解
- 速度慢
- 存储空间大
- 因为函数调用需要一直发送地址与分配地址
- 循环
- 不易理解
- 速度快
- 存储空间小
- 递归:
举例 - 求阶乘
- 阶乘的核心就是递归用n*(n-1),不断循环调用n-1其实就是将问题不断的缩减直至为1
- 使用循环实现递归,但由于使用int型所以不能输出大数字的递归,因为c语言没有定义长整型的长度,他只是定义长整型必须比整形大,而整形再这里只占4个字节
#include <stdio.h>
int main(){
int val;
int i,mult=1;
printf("please input a number: val= ");
scanf("%d",&val);
for(i=1;i<val;++i)
{
mult=mult*i;
}
printf("%d! is: %d\n",val,mult);
return 0;
}
举例 - 1+2+3+4+···+100的和(多个数字的相加)
- 与阶乘一样,核心就是递归用n+(n-1),不断循环调用n-1其实就是将问题不断的缩减直至为1
#include <stdio.h>
// sum(n),实现n个数相加
int sum(int n)
{
if (1==n)
return 1;
else
return f(n-1)+n;
}
int main()
{
printf("%d",sum(100));
return 0;
}
举例 - 汉诺塔
- ABC三个柱子可以看为栈,因为每次只能移动一个,而且是顶上的,就像是栈顶那样
- 汉诺塔的实现主要核心是伪代码的部分,就像是核心一样
#include <stdio.h>
int hannuota(int n,char A,char B,char C)
{
/*伪算法部分【盘分为`n`与`n-1`两个】
如果是1个盘子
将A柱的盘从A移动到C
否则
先将A柱上的n-1个盘从A借助C移动到B
将A柱上的n盘从A移动到C
最后将B柱上的n-1个盘从B借助A移动到C
*/
if(1==n)
printf("move %d: %c -> %c\n",n,A,C);
else
{
// n=1,从A借助C移动到B上
hannuota(n-1,A,C,B);
// 将n从A移动到C上
printf("move %d: %c -> %c\n",n,A,C);
// 将n-1从B借助A移动到C上
hannuota(n-1,B,A,C);
}
}
int main()
{
int n;
printf("please input number:");
scanf("%d",&n);
printf("\n");
hannuota(n,'A','B','C');
}
举例 - 走迷宫
递归的应用
- 树和森林就是以递归的方式定义的
- 数和图的很多算法都是以递归来实现的
- 很多数学公式就是以递归的方式定义的
应用 - 斐波那契数列
- 核心的公式是:当
n≤2时{n=1},n>2时{F(n-1)+F(n-2)}
- 后一项是前n项的和
斐波那契数列的四种实现方式cherrydreamsover的博客
-
递归
#include <stdio.h> // 递归实现斐波那契数列,核心公式是F(n-1)+F(n-2) int fibon(int n) { // 也可以是if(n<=2) // 当n为3,f(2)+f(1)=1+1=2 // 当n为4,f(3)+f(2)=[f(2)+f(1)]+f(2)=2+1=3 if(n<=2) { return 1; } else { return fibon(n-1)+fibon(n-2); } } int main(){ int n, result; printf("please input number:"); scanf("%d",&n); printf("\n"); result = fibon(n); printf("fibon result is :%d",result); return 0; }
非线性结构
树
树定义
- 专业定义
- 有且只有一个称为根的节点
- 有若干个互不相交的子树,这些子树本身也是一棵树
- 通俗的定义
- 树是由节点和边组成
- 每个节点只有一个父节点,但可以有多个子节点
- 但有一个节点例外,该节点没有父节点,此节点称为根节点
术语
- 节点
- 严格可表示为哪一个具体的树
- 父节点
- 最上面与其紧挨着的(父)节点
- 子节点
- 其下面所有的节点(子孙)
- 深度
- 从根节点到最底层节点的层数称之为深度
- 根节点是第一层
- 叶子节点
- 没有子节点的节点
- 非终端节点
- 实际就是非叶子节点(有子节点的节点)
- 根节点既可以是叶子节点也可以是非叶子节点
- 度
- 子节点的个数称之为度
- 若一子节点下有三个子节点,就称这个节点有3个度
- 树的度是整个树含有最大子节点的度数为整个树的度
树分类
一般树
- 任意一个子节点的个数都不受限制,一般的树都是无序的
- 一般的树可以是有序的树也可以是无序的,但二叉树一定是有序的树
二叉树
-
任意一个子节点的个数最多两个,且子节点的位置不可更改
- 任一子节点可以有一个也可以有两个,节点的顺序不能改变.二叉树是有序的树
-
二叉树的分类
-
一般二叉树
-
满二叉树
- 每一层都是最大的;在不增加层数的前提下,无法再多添加一个节点的二叉树就是满二叉树
-
完全二叉树⛳
- 完全二叉树是路径最短的二叉树,但路径最短的二叉树不一定是完全二叉树
- 如果只是删除了满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树
- 用数组实现二叉树必须是完全二叉树,在删除节点的时候必须从最地层从右往左删
- 满二叉树是完全二叉树的一个特例,完全二叉树包含了满二叉树
-
森林
- n个互不相交的树的集合,将下图的三个树叫做一个整体的话就是一个森林
树的存储
二叉树的存储
-
存储问题 详细
由于二叉树不是线性结构,由于计算机是线性的存储结构,所以要其以线性结构的方式保存,首先将其转换为完全二叉树,补完完全二叉树的节点,还原其完整的样子,得到其框架后保存,使用时再将补齐的部分删除(绿色为有效数据,蓝色为补充的框架)
最核心的是以二叉树来存储,一般树与二叉树都是转换为二叉树来存储,因为二叉树的算法比较成熟
- 连续存储(完全二叉树)
- 优点
- 查找某个节点的父节点和子节点(也包括判断有没有子节点)速度很快
- 缺点
- 耗用内存空间过大
- 优点
- 链式存储
一般树的存储
-
双亲表示法
- 求父节点方便
-
孩子表示法
- 求子节点方便
-
双亲孩子表示法
- 求父节点和子节点都很方便
-
二叉树表示法
- 把一个普通树转化成二叉树来存储
- 具体转换方法
- 左指针域指向它的第一个孩子
- 右指针域指向它的堂兄弟
- 只要满足此条件,就可以把一个普通树转换成二叉树
- 一个普通树转化成的二叉树一定没有右子树
森林的存储
- 先把森林转化为二叉树,再存储二叉树
- 将多个树合在一起存储,存储多个树时,把其他树当作兄弟来存储即可
树的操作 - 二叉树的遍历
先序遍历
-
遍历顺序(先访问根节点)
- 先访问根节点
- 再先序访问左子树
- 再先序访问右子树
-
举个例子
- A-B-D-C-E-F-G
- A-B-C-D-E-F-L-Q-M-N-S
- A-B-Q-L-C-D-G-E-F
中序遍历
-
遍历顺序(中间访问根节点)
- 中序遍历左子树
- 再访问根节点
- 再中序遍历右子树
-
举个例子
- B-D-C-E-A-C-F-N-Q-M
- 此处N后面可能会有点疑惑,这里解释下,因为到了MNQ这个子树,所以先判断M的左子树,即NQ树,首先遍历N的左子树(空),再遍历根节点(N),再遍历右子树(Q),再回头访问这个根节点(M),由于没有右子树,所以为空
- B-C-D-A-M-Q-E-L-N
- B-D-C-E-A-C-F-N-Q-M
后序遍历
-
遍历顺序(最后访问根节点)
- 后续遍历左子树
- 再后续遍历右子树
- 再访问根节点
-
举个例子
- B-D-M-F-L-E-C-A
- N-W-T-S-F-P-L-Q-M
已知两种遍历序列求原始二叉树
- 通过先序和中序或者中序和后序,我们可以还原出原始的二叉树,但是通过先序和后续是无法还原出原始的二叉树的。
- 也就是说:只有通过先序和中序或中序和后续我们才可以唯一的确定一个二叉树
已知先序和中序求后序
- 先序的第一个字母(A)一定是根节点
- 中序遍历在A左边的字母(BDCE)是左子树,右边是右子树(FHG)
- 重复1、2的操作
- 例1
- 先序:ABCDEFGH
- 中序:BDCEAFHG
- 求后序:DECBHGFA
-
先还原出二叉树
-
先序第一个字母A,即为根节点,中序里A的左边BCDE是左子树,右边FHG是右子树
-
先序第二个是B,中序第一个是B,即根节点的左子树的根是B,也因为中序第一个是B所以没有左子树
-
B的右子树剩下CDE,由于先序是CDE(C为根),中序是DCE(D在DE中间,即D为C的左子树,E为D的右子树),所以左子树的顺序就出来了
-
剩下FGH就是A的右子树,先序是FGH,中序是FHG,所以F是右子树的根节点
-
由于中序里是FHG,F左边没有内容所以GH是F的右子树,因为先序是GH,所以G为根节点
-
中序中由于H在G的左边所以H是G的左子树
-
-
原始二叉树:
-
所以后序的顺序是:DECBHGFA
-
- 例2
-
先序:ABDGHCEFI
-
中序:GDHBAECIF
-
后序:GHDBEIFCA
-
原始二叉树
-
这里由于例1所以简单描述
- 先序的A在中序中就可以判断GDHB是A的左子树,ECIF是A的右子树
- AB的B在中序中是GDHB,由于中序的B右边没其他了所以可以确定GDH是B的左子树
- 看先序中ABD,D在GH前面,中序是GDH,就可以判断D是GH的根节点,且D的左子树是G,H是D的右子树至此A的左子树已完成遍历
- 看A的右子树ECIF对应先序的CEFI,先序中C在前,中序中C在E和IF的中间,所以C是EIF的根节点,且E为左子树,IF为右子树
- 剩下IF,由于先序是FI,所以F是I的根节点,中序是IF,I在F的左边所以I是F的左子树
-
已知中序和后序求先序
-
由于没有好的总结所以这里稍微写下
- 后序最后面的即整个树的根节点,然后看其在中序中的位置,即可分开树的左子树与右子树
- 然后由于后序是后遍历左子树再后遍历右子树再访问根节点,所以能得出左右子树的上下层逻辑关系
- 然后再通过中序的左右位置判断其左右子树的关系
-
例1
- 中序:BDCEAFHG
- 后序:DECBHGFA
- 求先序:ABCDEFGH1
- 顺序
- 后序A在最后所以A是根节点,所以中序的BDCE是A的左子树,FHG是A的右子树
- 然后看后序DECB的B在最后,再看中序是从B开始所以B是DEC的根节点,且在中序中DCE在B的右边所以DCE是B的右子树
- 后序中DEC,中序是DCE,所以DE可以说C的子树,且中序是DCE所以D是C的左子树,E是C的右子树,至此A的左子树已经遍历完成
- 右子树中FHG后序是HGF,所以F是右子树的根,后序顺序是HGF,所以H是G的子树,G是F的子树,再看中序HG在F的右边,所以G是F的右子树,H在G的左边,所以H是G的左子树
-
例2
- 中序:GDHBAECIF
- 后序:GHDBEIFCA
- 求先序:ABDGHCEFI
- 这里简略说下,右子树中从后序来看可以看出关系是C>EIF,F>IE,从中序可以看出E是C的左子树,IF是C的右子树,所以F>I,即I是F的左子树
树的应用
- 树是数据库中数据组织的一种重要形式
- 操作系统子父进程的关系本身就是一棵树
- 面向对象语言中类的集成关系
- 赫夫曼树
实现【待】
图
- 由于教程没有图,所以额外补上
数据的逻辑结构
- 集合:数据元素间除“同属于一个集合”外,无其他关系
- 线性结构:一个对一个,如线性表、栈、队列
- 树形结构:一个对多个,如树
- 图形结构:多个对多个
图的定义
- 图:G=(V, E)
- V:定点(数据元素)的有穷非空集合
- E:边的有穷集合
- 无向图:每条边都是无方向的
- 有向图:每条边都是有方向的
-
完全图:任意两个点都有一条边相连
- 无向完全图:n个顶点,
n(n-1)/2
条边 - 有向完全图:n个顶点,
n(n-1)
条边
- 无向完全图:n个顶点,
-
稀疏图:有很少边或弧的图(e<nlogn)
-
稠密图:有较多边或弧的图
-
网:边/弧带权的图(带值的边/弧)
-
邻接:有边/弧相连的两个顶点之间的关系
- 存在$(v_i, v_j)$ ,则称$v_i,v_j$ 互为邻接点
- 无向图,
( )
表示不分先后
- 无向图,
- 存在$<v_i,v_j>$ ,则称$v_i$ 邻接到$v_j$, $v_j$ 邻接于 $v_i$
- 有向图,
< >
表示分先后顺序,从$v_i \to v_j$
- 有向图,
- 存在$(v_i, v_j)$ ,则称$v_i,v_j$ 互为邻接点
-
关联(依附):边/弧与顶点之间的关系
-
顶点的度:与该顶点相关联的边的树木,记为TD(v)
-
在有向图中,顶点的度等于该顶点的入度与出度之和
- 顶点b的入度时以b为终点的有向边的条数,记作ID(v)
- 顶点v的出度是以v为始点的有向边的条数,记作OD(v)
-
例:图中各顶点的度
-
-
例题
问:当有向图中仅1个顶点的入度为0其余顶点的入度均为1,此时是何形状?答:是树!而且是一棵有向树!
-
路径:接续的边构成的顶点序列
-
路径长度:路径上边或弧的数目/权值之和
- 回路(环):第一个顶点和最后一个顶点相同的路径
- 0→1→2→0
- 简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径
- 0→1→3→2
- 非简单路径:0和1出现了两次所以是非简单路径
- 简单回路(简单环):除路径起点和终点相同外,其余顶点均不相同的路径
-
连通图(强连通图)
- 在无(有)向图G=(V, {E})中,若对任何两个顶点v、u都存在从v到u的路径,则称G是连通图(强连通图)
-
权与网
- 图中边或弧所具有的相关数称为权。标明从一个顶点到另一个顶点的距离或耗费
- 带权的图称为网
-
子图
-
设有两个图G=(V, {E})、G1=(V1, {E1}),若$V1\subseteq V,E1\subseteq E$则称G1是G的子图
- 例:(b), (c)是(a)的子图
-
-
连通分量(强连通分量)
-
无向图G的极大连通子图称为G的连通分量
- 极大连通子图意思是:该子图是G连通子图,将G的任何不在该子图中的顶点加入,子图不再连通
-
强连通分量
-
有向图G的极大强连通子图称为G的强连通分量
- 极大强连通子图意思是:该子图是G的强连通子图,将D的任何不在该子图中的顶点加入,子图不再是强连通的
-
-
极小连通子图:该子图是G的连通子图,在该子图中删除一条边1子图都不再连通
-
生成树:包含无向图G所以顶点的极小连通子图
-
生成森林:对非连通图,由各个连通分量的生成树的集合
-
图的抽象类型定义
-
图的抽象数据类型定义如下:
ADT Graph{ 数据对象V:具有相同特性的数据元素的集合,称为顶点集 数据关系R:R={VR} VR={<V,W>|<V,W>|V,W∈V^p(V,W), <V,W>表示从V到W的弧,P(V,W)定义了弧<V,W>的信息 }
-
基本操作
基础操作P: Create_Graph():图的创建操作 初始条件:无 操作结果:生成一个没有顶点的空图G GetVex(G,v):求图中的顶点v的值 初始条件:图G存在,v是图中的一个顶点 操作结果:生成一个没有顶点的空图G ······ CreateGraph(&G,V,VR) 初始条件:V是图的顶点集,VR是图中弧的集合 操作结果:按V和VR的定义构造图G DFSTraverse(G) 初始条件:图G存在 操作结果:对图进行深度优先遍历 BFSTraverse(G) 初始条件:图G存在 操作结果:对图进行广度优先遍历 }ADT Graph
图的存储结构
- 图的逻辑结构:多对多
- 数组表示法(临界矩阵):图没有顺序存储结构,但可以借助二维数组来表示元素间的关系
- 链式存储结构
- 多重链表
- 邻接表
- 邻接多重表
- 十字链表
- 多重链表
- 重点:
- 邻接矩阵(数组)表示法
- 邻接表(链式)表示法
邻接矩阵
数组(邻接矩阵)表示法
-
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间关系)
-
无向图的邻接矩阵表示法
- 两个顶点之间如果有边(两个顶点是邻接关系),则邻接矩阵的值为1,反之为0
- 可以看到对角线上都为零,因为没有指向自身,且此矩阵是对称矩阵
- 总结
- 无向图的邻接矩阵是对称的
- 顶点i的度=第i行(列)中1的个数
- 两个顶点之间如果有边(两个顶点是邻接关系),则邻接矩阵的值为1,反之为0
-
完全图的话,除了自身其余都为1
- 完全图的邻接矩阵中,对角元素为0,其余
-
有向图的邻接矩阵表示法
- 顶点指向的顶点对应行的顶点置为1
- 不是对称的矩阵
- 行为出度,列为入度
- 总结:
- 有向图的邻接矩阵可能是不对称的
- 顶点的出度=第i行元素之和
- 顶点的入读=第i列元素之和
- 顶点的度=第i行元素之和+第i列元素之和
-
网(即有权图)的邻接矩阵表示法
- 顶点指向的顶点对应行的顶点置为权值的大小,没有被值的顶点则置为无穷
邻接矩阵的建立
-
邻接矩阵的存储表示:用两个数组分别存储顶点表和邻接矩阵
#define MVNum 100//最大定点数 typedef char VerTexType;//设顶点的数据类型为字符型 typedef int ArcType;//假设边的权值类型为整型 typedef struct{ VerTexType vexs[MVNum];//顶点表[一维数组] ArcType arcs[MVNum][MVNum];//邻接矩阵[二维数组] }AMGraph;//Adjacency Matrix Graph
-
采用邻接矩阵表示法创建无向网
- 无向网
- 无向网
- 有向图
- 有向网
- 算法思想
- 输入总顶点数和总边数
- 一次输入点的信息存入顶点表中
- 初始化邻接矩阵,使每个权值初始化为极大值
- 构造邻接矩阵
#define MaxInt 32767//表示极大值,即无穷 define MVNum 100//最大顶点数 typedef char VerTexType;//设顶点的数据类型为字符型 typedef int ArcType;//假设边的权值类型为整型 typedef struct{ VerTexType vexs[MVNum];//顶点表[一维数组] ArcType arcs[MVNum][MVNum];//邻接矩阵[二维数组] int vexnum,arcnum;//图的当前点数和边数 }AMGraph,G;//Adjacency Matrix Graph Status CreateUDN(AMGraph &G){ //采用邻接矩阵表示法,创建无向网G cin>>G.vexnum>>G.arcnum;//输入总项点数,总边数 for(i=0;i<G.vexnum;++i) cin>>G.vexs[i];//依次输入点的信息 //由于使二维数组,所以需要两个for循环 for(i=0;i<G.vexnum;++i)//初始化邻接矩阵 for(j=0;j<G.vexnum;++j) G.arcs[i][j]=MaxInt;//边的权值均置为极大值 for(k=0;k<G.arcnum;++k){ 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 }//for return OK; }//CreateUDN //补充用于在图中查找顶点 int LocateVex(AMGraph G, VertexType u){ //图G中查找顶点u,存在则返回顶点表中的下表; 否则返回-1 int i; for(i=0;i<G.vexnum;++i) if(i==G.vexs[i]) return i; return -1; }
- 无向网
邻接矩阵表示法的优缺点
- 优点
- 直观、间断、好理解
- 方便检查任意一对顶点间是否存在边
- 方便找任一顶点的所有“邻接点”(有边直接相连的顶点)
- 方便计算任一顶点的“度”(从该点发出的边数为“出度”,指向该点的边数为“入度”)
- 无向图:对应行(或列)非0元素的个数
- 有向图:对应行非0元素的个数是“出度”;对应列非0元素的个数是“入度”
- 缺点
- 不便于增加或删除节点
- 浪费空间——存稀疏图(点很多而边很少)有大量无效元素
- 对稠密图(特别是完全图)还是很合算的
- 浪费时间——统计稀疏图中一共有多少条边
邻接表
邻接表表示法(链式)
-
无向图的邻接表表示方式
- 顶点
- 按编号顺序将顶点数据存储在一堆数组中
- 关联同一顶点的边(以顶点为尾的弧)
- 用线性链表存储
- 特点
- 邻接表不为一(边的顺序可以互换[边没有顺序])
- 若无向图中有n个顶点、e条边,则其邻接表需n个头节点和2e个表节点。适宜存储稀疏图
- 无向图中顶点Vi的度为第i个单链表中的结点数
- 顶点
-
有向图的邻接表表示方式
-
邻接表记录出度,逆邻接表记录入度
-
邻接表找出度易,找入度难
-
逆邻接表找入度易,找出度难
-
特点
- 顶点vi的出度为第i个单链表中的结点个数
- 顶点vi的入度为整个当联邦中邻接点域值是i-1的结点个数
-
练习:已知某网的邻接(出边)表,请画出该图
-
图的邻接表存储特点
-
邻接表特点
- 方便找任一顶点的所有“邻接点”
- 节约稀疏图的空间需要N个头指针+2E个几诶单(每个节点至少2两个域)
- 方便计算任一顶点的“度”?
- 对无向图:是的
- 对有向图:指南计算“出度”;需要构造“逆邻接表”(存指向自己的边)来方便计算“入度”
- 不方便检查任意一对顶点间是否存在边
- 方便找任一顶点的所有“邻接点”
-
邻接矩阵与邻接表表示法的关系
- 联系:邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数
- 区别:
- 对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(邻接次序与顶点编号无关)
- 邻接矩阵的空间复杂度为O(n^2),而邻接表的空间复杂度为O(n+e)【更好】
- 用途:邻接矩阵多用于稠密图;而邻接表多用于稀疏图
-
邻接表的有向图与无向图的缺点补充【了解】
-
十字链表
-
存储有向图,解决求结点的度难度问题
-
简介
- 十字链表( Orthogonal List)是有向图的另一种链式存储结构。我们也可以把它看成是将有向图的邻接表和逆邻接表结合起来形成的一种链表。
- 有向图中的每一条弧对应十字链表中的一个弧结点,同时有向图中的每个顶点在十字链表中对应有一个结点,叫做顶点结点
-
-
链接多重表
-
存储无向图,每条变都要存储两遍问题
-
-
图的遍历
- 定义
- 从已给的连通图中某一项点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,他是图的基本运算查找和排序
- 图的特点
- 图中可能存在回路,且图的任一顶点都可能与其他顶点想通,再访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点
-
如何避免重复访问?
- 设置辅助数组
-
- 图中可能存在回路,且图的任一顶点都可能与其他顶点想通,再访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点
排序与查找
折半查找【待】
排序算法【待】
- 冒泡
- 数与数之间两两相比,升序的话大的放右边,一直比较下去,第一次循环过后最大的数就放在最右边,n*(n-1)次循环后不连续的n个数就排序完成了
#include <stdio.h> int main() { int list[5]; int len = sizeof(list) / sizeof(list[0]); int *p, c; p = list; printf("please input ten number:"); for (int n = 0; n < len; n++) scanf("%d", p++); int num = sizeof(list) / sizeof(list[0]); for (int i = 0; i < num-1; i++) { for (int j = i + 1; j < num; j++) { if (list[i] > list[j]) { c = list[i]; list[i] = list[j]; list[j] = c; } } for (int k = 0; k < 5; k++) { printf("%d ", list[k]); }printf("\n"); } for (int b = 0; b < len; b++) { printf("%d ", list[b]); } }
- 插入
- 左所有的数字里面,升序的选择第i个数,往后判断,如果比它小则互换位置,经过第n次比较后这个不连续的n*(n-1)个数就排序完成了
#include <stdio.h> int main() { int list[]={9,5,7,3,1,6}; int poc; int box; int i,j; int len=sizeof(list)/sizeof(list[0]); for(i=1;i<len;i++) { box=list[i]; for(j=i-1;j>=0&&list[j]>box;j--) { list[j+1]=list[j]; } list[j+1]=box; } for(int k=0;k<6;k++) { printf("%d ",list[k]); } }
- 选择
- 从所有的元素里面找到最小的,再与第一个互换,然后在剩下的元素里进行查找和互换
#include <stdio.h> int main() { int list[]={9,5,7,3,1,6}; int len = sizeof(list) / sizeof(list[0]); int min,box,i,j; for(i=0;i<len;i++) { min = i; for(j=i+1;j<len;j++) { if(list[j]<list[min]) min = j; } box=list[i]; list[i]=list[min]; list[min]=box; } for (int k = 0; k < 6; k++) { printf("%d ", list[k]); } }
- 快速排序
- 设置l和r下标,将数组分为三段,a[l:i-1], a[i], a[i+1:r],然后一直递归对a[l:i-1]和 a[i+1:r]排序互换位置,最后再对a[l:i-1], a[i], a[i+1:r]何必
- 归并排序
- 先两个两个数两两排序,再四个数列四四排序...
- 排序和查找的关系
- 排序时查找的前提
- 排序时重点
刷题
专业课刷题
-
C语言常常称为中级语言,方便移植
-
9和字符 '9' 不相等!!
9!='9'
,值为1,因为类型都不一样 -
字符串长度包括所有字符和空格字符的个数
-
常用算法的使用(注意,考试中考察下列算法大多是填写空白填空,所以得知道核心算法!还有代码的先后顺序)
-
素数
- 素数是指除了1和它本身之外,不能被任何整数整除的数
- 只要不被大于一、小于其本身的数%为0则是素数
-
阶乘
-
闰年
- 分开
- 先
if(year%400==0)
- 再嵌套
if(year%4==0 && year%100!=0)
- 先
- 或直接
if(year%4==0 && year%100!=0 ||year%400==0))
- 分开
-
每个数的次方相加
-
斐波那契数列的递归算法
if (n<=2) return 1; else return fibon(n-1)+fibon(n-2)
-
二分查找的递归算法
-
快速排序的代码实现
-
-
switch ··· case ··· default ···
-
数据不可分割的单位是数据项,数据元素是基本单位,数据项是最小单位不可分割
-
时间复杂度是运行时间,空间复杂性是运行时候程序所占用空间变量
-
时间复杂性有最好情况和最坏情况,一般指的是平均复杂度
-
数据存储结构分四类:顺序存储、链式存储、索引存储、散列存储
-
拓扑序列是有序列表,除了拓扑序列之外其他都是无序列表
-
表达式求值进行栈计算
-
哈希表(hash)是解决栈溢出的问题
-
一个长度为 n 的链式队列入队的复杂性是o(1)
-
树
-
概念
-
度、叶结点、深度
- 结点的度
- 结点的孩子树(数量)
- B的度有两个度EF;F有两个度IJ
- 一棵树的度
- 该树中节点的最大度数
- B树的度为2
- 叶结点
- 度为0的结点叫做叶结点
- E的度为0,因为他没有孩子树,所以他是叶结点
- 树的深度(高度)
- 这棵树的层数
- B这棵树的深度为3
- 结点的度
-
二叉树
- 二叉树中每个节点至多两个结点(至多≤2)
- 性质
- 高度为h≥2的二叉树至少有h+1个结点
- 高度不超过h的二叉树至多右$2^{h+1}$个结点
-
满二叉树(拥有奇数个结点)
- 一棵树高度为h且有$2^{h+1}-1$个结点的二叉树称为满二叉树
- 除最低下的叶子节点,其他的结点都是2,若有一个结点不满足则不是满二叉树
- 一棵树高度为h且有$2^{h+1}-1$个结点的二叉树称为满二叉树
-
完全二叉树
- 结点是连续的,若不连续则不是完全二叉树,最下面一层上的结点都几种再该层最左边,完全二叉树是近似于满二叉树的
- 满二叉树一定是完全二叉树,完全二叉树不一地呢是满二叉树
-
森林的先中后序遍历,先使用左儿子右兄弟的方法整理一次后再进行先中后序遍历
-
-
二叉树的分支最多为2
-
二叉树的形态有5种
-
m 个节点的二叉树,对应的二叉链表有m+1个非空域
-
二叉树的深度遍历可以采用的数据结构堆
-
深度为n的二叉树最多有$n^{(n-1)}$个节点
-
具有 n 个叶子节点的哈夫曼树,共有 2*n-1 个节点
-
计算二叉树的最小高度:$\log_2h=n$,n为结点,h为高度;例如2000个结点,$log_211=2048$所以最小高度为11,(2^10=1024)
-
二叉树的左孩子结点是$2i$,右孩子结点是$2i+1$
-
满二叉树的高度为n(根为第1层),则该二叉树的结点总数为$2n-1$个。叶子结点右$2$个
-
二叉判断树,大概就是选取中间,按树的思路数列从左到右放,剩下的余数放在右子树即可
-
计算二叉树平均查找长度:(第n层 * 第n层节点的数量)/总节点数
-
假设树是有四层高度,如图所示,那么应为$(1+22+34+4*3)/10=2.9$
-
-
-
图
- n 个节点的联通图至少有n-1条边
- 查找关键字的信息搜索信息范围采用图的深度遍历(搜索边界速度快)
- 查找关键字的信息搜索精确度采用图的广度遍历(搜索所有有关的内容速度快)
- 中国铁路网信息是图的数据结构
-
排序⭐
- 冒泡排序
- 平均时间复杂度为$o(n^2)$
- 插入排序
- 平均时间复杂度为$o(n^2)$
- 选择排序
- 平均时间复杂度为$o(n^2)$
- 快速排序
- 平均时间复杂度为$o(n\log_{2}{n})$
- 过程:
- 先选定键(默认为最左边的数)
- 设置L(最左)与R(最右)指标
- L指向最左边的下标,从左往右判断是否比键大,若大则与R交换,否则L+1(往右)
- R指向最右边的下标,从右往左判断是否比键小,若小则与L交换,否则R-1(往左)
- 先从R开始与键判断,重复执行直到L与R重合,则第一次执行完毕
- 冒泡排序