目录
绪论
数据元素、数据项:
数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。
一个数据元素可由若干个数据项组成,数据项是构成数据元素的不可分割的最小单位。
数据结构、数据对象:
数据结构是相互之间存在一种或多种特定关系的数据元素集合。
数据对象是具有相同性质的数据元素的集合,是数据的一个子集。
一、数据的逻辑结构:
集合、线性、树形、图
二、数据的物理结构(存储结构):
1.顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中。需要分配连续的存储空间
2.链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
3.索引存储:在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,一般形式是(关键字,地址)
4.散列存储(哈希存储):根据元素的关键字直接计算出该元素的存储地址。
数据的存储结构会影响存储空间分配的方便程度和对数据运算的速度。
三、数据的运算
运算的定义是针对逻辑结构的,运算的实现是针对存储结构的。
算法
算法的基本概念
算法的时间复杂度
算法的空间复杂度
线性表
线性表的定义和基本操作
顺序表的定义
静态分配
#include <stdio.h>
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的“数组”存放数据元素
int length; //顺序表当前长度
}SqList; //顺序表类型定义
//初始化顺序表
void InitList(SqList &L){
for(int i=0; i<MaxSize; i++)
L.data[i]=0; //将所有数据元素设置为默认初始值(可省略
L.length=0; //顺序表初始长度为0
}
int main()
{
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//...其他操作
return 0;
}
动态分配
#include <stdlib.h> // malloc、free函数的头文件
#define InitSize 10 //默认最大长度
typedef struct{
int *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
//初始化顺序表
void InitList(SeqList &L)
{
//用malloc函数申请一片连续的存储空间
L.data=(int *)malloc(InitSize*sizeof(int));
L.length=0;
L.MaxSize=InitSize;
}
//增加动态数组的长度
void IncreaseSize(SeqList &L, int len)
{
int *p=L.data;
L.data=(int *)malloc((L.MaxSize+len)*sizeof(int));
for( int i=0; i<L.length; i++)
{
L.data[i]=p[i]; //将数据复制到新区域
}
L.MaxSize=L.MaxSize+len; //顺序表最大长度增加len
free(p); //释放原来的内存空间
}
顺序表的插入删除
插入
Listinsert(&L,i,e); 在表L中的位序i上插入元素e。
若要给使用者反馈是否插入成功,可用bool类型
删除
ListDelete(&L,i,&e); 删除表L中位序i处的元素,并用e返回删除元素的值
顺序表的查找
GetElem(L,i); 按位查找操作,获取表L中位序i处元素的值,时间复杂度O(1)
LocateElem(L,e); 按值查找操作,在表L中查找具有给定关键字的元素
单链表的定义
强调这是一个单链表----使用LinkList
强调这是一个结点----使用LNode *
不带头结点的单链表
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素
struct LNode *next; //指针指向下一个结点
}LNode, *LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L){
L = NULL; //空表,暂时没有任何结点
return true;
}
带头结点的单链表
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素
struct LNode *next; //指针指向下一个结点
}LNode, *LinkList;
//初始化
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点
if (L==NULL) //内存不足分配失败
return false;
L->next = NULL; //头结点之后暂时还没有结点
return true;
}
带头结点,写代码更方便。
单链表的插入删除
按位序插入(带头结点)
最好时间复杂度O(1):插在表头
最坏时间复杂度O(n):插在表尾
平均时间复杂度O(n)
bool ListInsert(LinkList &L,int i, ElemType e){
if(i<1)
return false;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点
while (p!=NULL && j<i-1){ //循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL) //i值不合法(即插入的位置还在链表尾部的后面)
return false;
LNode *s=(LNode *)malloc(sizeof(LNode)); //给新结点分配内存
s->data = e;
s->next = p->next;
p->next =s; //将结点s连接到p之后
return true; //插入成功
}
按位序插入(不带头结点)
如果不带头结点,则插入删除第1个元素时,需要更改头指针L。
//需在上面基础上增加一段代码
if(i==1){
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = L;
L = s; //头指针指向新结点
return true;
}
//后面代码和上面相同,注意j=1
指定结点的后插操作
可用 return InsertNextNode(p,e); 直接代替前面的找到i-1个结点后的操作
时间复杂度O(1)
//后插:在p结点之后插入元素e
bool InsertNextNode (LNode *p, ElemType e){
if(p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s==NULL) //内存分配失败
return false;
s->data = e; //用结点s保存数据元素e
s->next=p->next;
return true;
}
指定结点的前插操作
如果已知头指针
bool InsertPriorNode (LinkList L, LNode *p, ElemType e)
传入一个头指针,循环查找p的前驱结点q,再对q后插
时间复杂度O(n)
//未知头指针,在p结点前插入元素e
bool InsertPriorNode (LNode *p, ElemType e){
if (p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s==NULL) //内存分配失败
return false;
s->next=p->next;
p->next=s; //新结点s连到p之后
s->data=p->data; //将p中元素复制到s中
p->data=e; //p中元素覆盖为e
return true;
}
时间复杂度O(1)
按位序删除(带头)
最坏平均时间复杂度:O(n)
最好时间复杂度:O(1)
bool ListDelete(LinkList &L, int i, ElemType &e){
if(i<1)
return false;
LNode *p;
int j=0;
p = L;
while (p!=NULL && j<i-1){
p=p->next;
j++;
}
if(p==NULL)
return false;
if(p->next == NULL) //第i-1个结点之后已无其他结点
return false;
LNode *q=p->next; //令q指向被删除结点
e = q->data; //用e返回元素的值
p->next=q->next; //将*q结点从链中断开
free(q); //释放结点的存储空间
return true;
}
指定结点的删除
删除结点p,需要修改其前驱结点的next指针
方法1:传入头指针,循环寻找p的前驱结点 On
方法2:类似于结点前插的实现(但如果需删除结点是最后一个结点则不适用) O1
单链表的查找
只讨论带头结点的情况
GetElem(L,i); 按位查找操作,获取表L中第i个位置元素的值 平均On
LocateElem(L,e); 按值查找操作。在表L中查找具有给定关键字的元素 平均On
按位查找
//按位查找,返回第i个元素(带头结点)
LNode *GetElem(LinkList L, int i){
if(i<0)
return NULL;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点
while (p!=NULL && j<i){ //循环找到第i个结点
p=p->next;
j++;
}
return p;
}
按值查找
LNode *LocateElem(LinkList L,ElemType e){
LNode *p = L->next;
//从第1个结点开始查找数据域为e的结点
while(p!= NULL && p->data!=e)
p=p->next;
return p; //找到后返回该结点指针,否则返回NULL
}
单链表的建立
讨论带头结点的情况
头插法、尾插法:核心就是初始化操作、指定结点的后插操作
尾插法
建立一个表尾指针 On
LinkList List_TailInsert(LinkList &L){ //正向建立单链表
int x; //设ElemType为整形
L=(LinkList)malloc(sizeof(LNode)); //建立头结点
LNode *s,*r=L; //r为表尾指针
scanf("%d",&x); //输入结点的值
while(x!=9999){ //输入9999表示结束
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s; //r指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL; //尾结点指针置空
return L;
}
头插法
LinkList List_TailInsert(LinkList &L){ //逆向建立单链表
LNode *s;
int x;
L=(LinkList)malloc(sizeof(LNode)); //建立头结点
L->next=NULL; //初始为空链表(避免指向脏数据
scanf("%d",&x); //输入结点的值
while(x!=9999){ //输入9999表示结束
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
s->next=L->next;
L->next=s; //将新结点插入表中,L为头指针
scanf("%d",&x);
}
return L;
}
双链表
typedef struct DNode{ //定义双链表结点类型
ElemType data; //数据域
struct DNode *prior,*next; //前驱和后继指针
}DNode,*DLinklist;
//初始化双链表
bool InitDLinkList(DLinklist &L){
L = (DNode *)malloc(sizeof(DNode)); //分配一个头结点
if(L==NULL) //内存不足分配失败
return false;
L->prior = NULL; //头结点的prior永远指向NULL
L->next = NULL; //头结点之后暂时没有结点
return true;
}
双链表的插入
//在p结点后插入s结点
bool InsertNextDNode (DNode *p, DNode *s){
if (p==NULL) || s==NULL) //非法参数
return false;
s->next=p->next;
if (p->next !=NULL) //如果p结点有后继节点
p->next->prior=s;
s->prior=p;
p->next=s;
return true;
}
双链表的删除
//删除p的后继节点q
p->next = q->next;
if (q->next !=NULL) //q结点不是最后一个结点
q->next->prior = p;
free(q);
双链表的遍历
1.后向遍历
while(p!=NULL){
p = p->next;
}
2.前向遍历
while(p!=NULL){
p = p->prior;
}
3.前向遍历(跳过头结点)
while(p->prior != NULL){
p = p->prior;
}
双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。时间复杂度On
循环链表
循环单链表
初始化循环单链表时,要 L->next = L; (链表为空时)
循环双链表
表头结点的prior指向表尾结点
表尾结点的next指向头结点
初始化循环单链表时,要 L->prior = L; L->next = L;
静态链表
初始化静态链表:把 a[0] 的 next 设为-1
查找:从头结点出发挨个往后遍历结点 On
插入位序为 i 的结点:1)找到一个空节点,存入数据元素
2)从头结点出发找到位序为 i-1 的结点
3)修改新结点的next
4)修改 i-1 号结点的next
优点:增、删操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变.
适用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)
栈
栈的基本概念
栈(Stack)是只允许在一端进行插入和删除操作的线性表
后进先出(LIFO)
栈的顺序存储实现
//顺序栈的定义
#define MaxSize 10
typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶指针
}SqStack;
//初始化操作
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}
//进栈操作
bool Push(SqStack &S,ElemType x){
if(S.top == MaxSize-1) //栈满,报错
return false;
S.top = S.top + 1; //指针先加1
S.data[S.top]=x; //新元素入栈
return true;
}
//出栈操作
bool Pop(SqStack &S,ElemType &x){
if(S.top==-1) //栈空,报错
return false;
x = S.data[S.top]; //栈顶元素先出栈
S.top = S.top-1; //指针再减1
return true;
}
栈的链式存储实现
队列的基本概念
队列(Queue)只允许在一端进行插入,在另一端删除的线性表(FIFO)
队列的顺序存储实现
#define MaxSize 10
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front, rear; //队头和队尾指针
}SqQueue;
//初始化队列
void InitQueue(SqQueue &Q){
Q.rear=Q.front=0;
}
//入队操作
bool EnQueue(SqQueue &Q, ElemType x){
if((Q.rear+1)%MaxSize==Q.front) //判断队满
return false;
Q.data[Q.rear]=x; //将x插入队尾
Q.rear=(Q.rear+1)%MaxSize //若队头空了,这样可以回到队头,称作循环队列
return true;
}
//出队操作(删除一个队头元素,并用x返回
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.rear==Q.front) //判断队空
return false;
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize;
return true;
}
队列元素的个数:
(rear+MaxSize-front)%MaxSize
上述判断队满的方式,需要在队尾浪费一个存储空间,否则判断队空和队满的条件会一样
解决这个问题的办法:
//方法一
#define MaxSize 10
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front, rear; //队头和队尾指针
int size; //队列当前长度
}SqQueue;
//插入成功size++,删除成功size--
//初始化时,rear=front=0; size=0;
//队满条件size==MaxSize 队空条件size==0
//方法二
#define MaxSize 10
typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front, rear; //队头和队尾指针
int tag; //最近进行的是删除/插入操作(0/1)
}SqQueue;
//初始化时,rear=front=0; tag=0;
//每次删除成功时,tag=0; 插入成功时,tag=1;
//队满条件:front==rear && tag==1;
//队空条件:front==rear && tag==0;
/*只有删除操作,才可能导致队空
/*只有插入操作,才可能导致队满
需要注意:队尾指针是指向队尾元素的后一个位置 还是指向队尾元素。
以上是指向后一个位置
队列的链式存储
树
树的定义与基本术语
树的性质
(1)结点数=总度数+1
(2)度为m的树、m叉树的区别
二叉树的定义和基本术语
二叉树的性质
二叉树的存储结构
二叉树的先中后序遍历
二叉树的层序遍历
由遍历序列构造二叉树
线索二叉树
二叉树的线索化
在线索二叉树中找前驱后继
树的存储结构
双亲表示法
新增数据元素,无需按逻辑上的次序存储
删除数据元素:
1.删除叶子结点,将该节点双亲指针改为-1,用尾结点填补空缺
2.删除子树根节点,需要将这个子树全部删除,但查找孩子结点只能从头到尾遍历
孩子表示法
孩子兄弟表示法
森林和二叉树的转换
树和森林的遍历
树的遍历
森林的遍历
哈夫曼树
带权路径长度
哈夫曼树定义
哈夫曼树构造
哈夫曼编码
图
图的基本概念
图的存储
邻接矩阵法
有时候自己指向自己也可用0表示
邻接表法
十字链表法
邻接多重表
图的基本操作
图的遍历
图的广度优先遍历 BFS
图的深度优先遍历 DFS