第一章 绪论(略)
第二章 线性表
2.1线性表的概念
2.1.1线性表的定义与特点
1.线性表的定义
线性表:是具有相同数据类型的n(n>=0)个数据元素的有序序列。
2.线性表的特点
①同一性:元素的数据类型无限制,但所有元素数据类型得一样。
②有序性:
除了第一个元素外,每一个元素有且仅有一个直接前驱。
除了最后一个元素外,每一个元素有且仅有一个直接后继。
每一个元素在线性表中的位置取决于它的序号。
③有限性:当n=0,线性表为空。
2.1.2线性表的基本运算
//线性表的初始化
InitList(L);
//求线性表的长度
LengthList(L);
//取表元
GetList(L, i);
//按值查找
LocationList(L, x);
//插入
InsertList(L, i, x);
//删除
DeleteList(L, i);
2.1.3线性表的抽象数据类型的定义
定义包括三个方面的内容:数据对象,元素之间的关系,定义在数据对象上的运算集
ADT 线性表
{
数据对象:
同一数据类型的元素有限集合
元素之间的关系:
第一个元素无直接前驱,最后一个元素无直接后驱;其余每一个元素有唯一的前驱和后驱
运算集:
插入,删除,查找等
}
2.2线性表的顺序存储
2.2.1线性表顺序存储与特点
1.线性表的顺序存储
线性表的顺序存储:在内存中用地址连续的一块存储空间对线性表中的各元素按其逻辑顺序依次存放。
顺序表:用顺序存储形式存储的线性表。
2.线性表顺序存储的特点
具有逻辑上相邻的两个元素其存储位置也相邻。
2.2.2顺序表基本运算的实现
1.顺序表类型定义
#define MAXSIZE 100 //顺序表可存储的最大元素的个数
typedef int DataType; //数据类型以int为例
typedef struct
{
DataType data[MAXSIZE];
int len; //顺序表的表长
}SeqList;
SeqList L1; //定义一个顺序表变量L1
SeqList* L2; //定义一个SeqList类型的指针变量L2,保存顺序表起始地址
L2 = (SeqList*)malloc(sizeof(SeqList)); //顺序表存储空间动态获得
2.顺序表上基本运算的实现
(1)顺序表的初始化
//建立一个空的顺序表L
SeqList* InitList()
{
SeqList* L; //定义顺序表指针变量L
L = (SeqList*)malloc(sizeof(SeqList)); //动态顺序表空间申请
L->len = 0; //表长为0
return L;
}
(2)按值查找
//顺序表L中查找值为x的元素
int LocationList(SeqList* L, DataType x)
{
int i = 1; //从下标为1开始存放元素
while (i<=L->len&&L->data[i]!=x)
{
i++;
}
if (i > L->len)
return 0;
else
return i;
}
(3)插入
//在顺序表L的第i个位置插入一个值为x的新元素
int InsertList(SeqList* L, int i, DataType x)
{
int j;
if (L->len == MAXSIZE - 1)
{
printf("表满");
return -1;
}
if (i<1 || i>(L->len + 1))
{
printf("插入位置有误");
return 0;
}
for (j = L->len; j >= i; j--)
{
L->data[j + 1] = L->data[j];
}
L->data[i] = x;
L->len++;
return 1;
}
(4)删除
//从L所指向的顺序表中删除第i个元素
int DeleteList(SeqList* L, int i)
{
if (L->len == 0)
{
printf("表为空");
return -1;
}
if (i<1 || i>L->len )
{
printf("删除位置有误");
return 0;
}
for (int j = i+1; j <= (L->len); j++)
{
L->data[j-1] = L->data[j];
}
L->len--;
return 1;
}
2.2.3线性表顺序存储的优缺点
优点:①无须为表示元素之间的逻辑关系而增加额外的存储空间。
②按元素序号随机访问表中的任一元素。
缺点:①插入,删除操作,效率低。
②需要预先分配足够大的空间。
空间过大:导致空间利用率低;空间过小:造成溢出。
③静态内存分配的方法,表难以扩充。
2.3线性表的链式存储
2.3.1线性表链式存储与特点
1.链表的概念
链表:线性表的链式存储结构。
2.链表的特点
①所有结点的存储空间不一定连续。
②可以实现动态内存分配。
2.3.2链表的运算
1.结点的定义
//单链表结点的定义
typedef int DataType;
typedef struct node
{
DataType data;
struct node* next;
}Lnode,*LinkList;
//动态申请结点空间并将申请到的空间地址赋值给指针变量p
LinkList p;
p = (LinkList)malloc(sizeof(Lnode));
在单链表的插入、删除运算中,因为运算的位置可能在头,中间,尾,为了使其在不同位置上实现的运算保持统一,引入了带头结点的单链表。
//head是一个指针变量
LinkList head;
2.建立
🎈方法一:头插法
//头插法建立带头结点的单链表,返回头头指针head
LinkList CreatList(LinkList head)
{
LinkList s;
head = (LinkList)malloc(sizeof(Lnode)); //申请头结点的空间
head->next = NULL; //形成空表
int n = 0;
scanf("%d", &n); //输入结点个数
for (int i = n; i > 0; i--)
{
s = (LinkList)malloc(sizeof(Lnode)); //申请新结点空间
scanf("%d", &s->data); //输入新结点的数据
s->next = head->next; //将新结点插到头结点head之后
head->next = s;
}
return head;
}
✅图解:(之后学会gif制作将用动态方式展现)
注意:头插法放入的数据的反着的
🎈方法二:尾插法
//尾插法建立带头结点的单链表,返回头头指针head
LinkList CreatList(LinkList head)
{
LinkList p;
head = (LinkList)malloc(sizeof(Lnode));
p = head;
int n = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
p->next = (LinkList)malloc(sizeof(Lnode));
p = p->next;
scanf("%d", &p->data);
}
p->next=NULL;
return head;
}
✅图解:较为简单不做赘述。
3.插入
int InsertList(LinkList head, int i, DataType x)
{
int k = 0;
LinkList p,s;
p = head;
while (p!=NULL&&k<i-1) //确定插入的位置,p最终指向第i-1个结点
{
p = p->next;
k++;
}
if (p == NULL)
{
printf("位置不合理");
return 0;
}
s = (LinkList)malloc(sizeof(Lnode));
s->data = x;
s->next = p->next;
p->next = s;
return 1;
}
4.删除
int DeleteList(LinkList head, int i)
{
int k = 0;
LinkList p,r;
p = head;
while (p!=NULL&&k<i-1)
{
p = p->next;
k++;
}
if (k > i - 1 || !(p->next))
{
printf("位置不合理");
return 0;
}
r = p->next; //保存待删结点的位置
p->next = r->next; //原链指向r下一个结点
free(r); //释放r
return 1;
2.3.3单向循环链表
p->next=head;
2.3.4双向链表及其运算
1.结点的定义
//双向链表结点的定义
typedef int DataType;
typedef struct Lnode
{
DataType data;
struct Lnode* prior, * next;
}DLnode,*DLinkList;
2.插入
s->prior = p->prior; //1
p->prior->next = s; //2
s->next = p; //3
p->prior = s; //4
3.删除
p->prior->next = p->next; //1
p->next->prior = p->prior; //2
free(p);
2.3.5静态链表
//数组sd定义如下:
#define MAXSIZE 1000
typedef int DataType;
typedef struct
{
DataType data;
int next;
}SNode;
SNode sd[MAXSIZE];
int SL, AV;
规模足够大的结构数组
sd[MAXSIZE]
中有两个链表:
SL
是一个带头结点的单链表,表示了线性表(a1, a2, a3, a4, a5)
另一个单链表AV
是将当前sd
中的空结点组成的链表
2.3.6链式存储结构的优缺点
优点:①扩充方便。
②使用灵活。
③节省空间。
缺点:①复杂,任意data的地址未知。
第三章 栈与队列
3.1栈
3.1.1栈的基本概念与特点
栈:限定仅能在表尾一端进行插入、删除操作的线性表。
特点:后进先出
3.1.2顺序栈的运算
1.顺序栈的定义
顺序栈:利用顺序存储方式实现的栈。
//顺序栈类型定义
#define MAXSIZE 100
typedef int DataType;
typedef struct
{
DataType data[MAXSIZE];
int top; /* 栈顶指针 */
} SeqStack;
SeqStack* S;
S = (SeqStack*)malloc(sizeof(SeqStack));
2.置空栈
//方法一:二级指针
void InitSeqStack(SeqStack** S)
{
*S = (SeqStack*)malloc(sizeof(SeqStack));
(*S)->top = -1;
}
//方法二:一级指针
SeqStack* InitSeqStack(SeqStack* S)
{
S = (SeqStack*)malloc(sizeof(SeqStack));
S->top = -1;
return S;
}
3.判空栈
int EmptySeqStack(SeqStack* S)
{
if (S->top == -1)
return 1;
else
return 0;
}
4.入栈
int PushSeqStack(SeqStack* S, DataType x)
{
if (S->top == MAXSIZE - 1)
{
printf("栈满");
return 0;
}
S->top++;
S->data[S->top] = x;
return 1;
}
5.出栈
int PopSeqStack(SeqStack* S, DataType *x)
{
if (S->top == -1)
{
printf("栈空");
return 0;
}
*x = S->data[S->top]; //保存了栈顶元素
S->top--;
return 1;
}
补充知识:多个栈相同空间的共享
它主要利用了栈“栈底位置不变,而栈顶位置动态变化”的特性
目的:节省空间
//共享相同空间的双栈类型定义
typedef struct
{
DataType data[MAXSIZE];
int top[2]; /* 栈顶指针 */
}DqStack;
3.1.3链栈的基本概念
链栈:用链式存储结构实现的栈。
由于只能在链表头部进行操作,故链栈没有必要像单链表那样附加头结点。栈顶指针就是链表的头指针。
3.1.4链栈的运算
1.链栈的定义
typedef int DataType;
typedef struct node
{
DataType data;
struct node* next;
} StackNode;
StackNode* top; // top为栈顶指针
2.置空栈
//方法一:
void InitLinkStack(StackNode** top)
{
*top = NULL;
}
//方法二:
StackNode* InitLinkStack(StackNode* top)
{
top = NULL;
return top;
}
3.判空栈
int EmptyLinkStack(StackNode* top)
{
if (top == NULL)
return 1;
else
return 0;
}
4.入栈
void PushLinkStack(StackNode** top,DataType x)
{
StackNode* p;
p = (StackNode*)malloc(sizeof(StackNode));
p->data = x;
p->next = *top;
*top = p;
}
5.出栈
int PopLinkStack(StackNode** top, DataType *x)
{
StackNode* p;
if (top == NULL)
{
printf("栈为空");
return 0;
}
*x = (*top)->data; //保存栈顶元素
p = *top;
*top = (*top)->next;
return 1;
}
3.2 队列
3.2.1 队列的基本概念及特点
定义:限定仅能在表头进行删除、表尾进行插入的线性表。
🔵特点:先进先出(First In First Out)
3.2.1 队列的运算
1.队列的定义
#define MAXSIZE 100
typedef int DataType;
typedef struct
{
DataType data[MAXSIZE];
int front, rear; /*队头、队尾指针*/
}SeQueue;
SeQueue* sq; /*定义一个指向队的指针变量*/
sq = (SeQueue*)malloc(sizeof(SeQueue));
🔴注意:sq->front
保存队头前面元素的位置, sq->rear
保存队尾元素位置
2.入队
🔵特点:front
不变,rear
变
3.出队
🔵特点:rear
不变,front
变
3.2.3 队列的假溢出
❗问题描述:
满足:①rear = MAXSIZE - 1
②front≠ - 1
✅解决方案:
方法一:尽可能设置较大的队列容量
缺点:浪费空间
方法二:修改出队列算法,每次出队移动剩余元素
缺点:移动浪费时间,时间复杂度O(n)
方法三:修改入队列算法,当假溢出时,则把队列中的元素向队头移动front+1
个位置。
缺点:移动浪费时间,时间复杂度O(n)
方法四:循环队列
设想队列的存储空间向量是一个头尾相接的循环向量。
✅一般两种方法解决队空队满条件相同的问题:
方法一:另设一个变量num
记录当前队列中的元素个数, 当num == 0
时队空, num == MAXSIZE
时队满。
方法二:少用一个存储单元(即少存一个元素), 队满条件为rear + 1 == front
。
3.2.4 循环队列的运算
1.循环队的定义
#define MAXSIZE 100
typedef int DataType;
typedef struct
{
DataType data[MAXSIZE];
int front, rear;
}C_SeQueue;
2.循环队列置空队
C_SeQueue* Int_SeQueue()
{
C_SeQueue* q;
q = (C_SeQueue*)malloc(sizeof(C_SeQueue));
q->front = q->rear = 0; /* 或MAXSIZE-1等 */
return q;
}
3.循环队判空栈
int Empty_SeQueue(C_SeQueue* q)
{
if (q->front == q->rear) return 1;
else return 0;
}
4.循环队入队
int In_SeQueue(C_SeQueue* q, DataType x)
{
if ((q->rear + 1) % MAXSIZE == q->front)
{
printf("队满");
return -1;
}
else
{
q->rear = (q->rear + 1) % MAXSIZE;
q->data[q->rear] = x;
return 1;
}
}
5.循环队出队
int Out_SeQueue(C_SeQueue* q, DataType* x)
{
if (q->front == q->rear)
{
printf("队空");
return -1;
}
else
{
q->front = (q->front + 1) % MAXSIZE;
*x = q->data[q->front]; //暂存出队的数据
return 1;
}
}
第四章 串、数组和广义表
4.1 串的概念
串的定义:数据元素仅由一个字符组成。和线性表一样,它通常采用的存储方式也是顺序存储和链式存储。
4.2 数组的存储结构
一维数组:只要顺序存放在连续的内存单元即可。
二维数组:将二维数组挤入一维的地址中,有两个策略:
①以行为主序(C、PASCALBASIC、COBOL等)
②以列为主序(FORTRAN)
(如图:行列主序的图解)
- 数组元素存储地址的计算
假设二维数组A每个元素占用s 个存储单元, Loc( a i j a_{ij} aij)为元素 a i j a_{ij} aij 的存储地址,Loc( a 00 a_{00} a00) 是 a 00 a_{00} a00存储位置, 也是二维数组A
的基址。
① 行序为主序,则元素 a i j a_{ij} aij 的存储位置可由下式确定: Loc( a i j a_{ij} aij) = Loc( a 00 a_{00} a00) + (n* i + j ) *s
② 列序为主序,则元素 a i j a_{ij} aij的存储位置可由下式确定: Loc( a i j a_{ij} aij) =Loc( a 00 a_{00} a00) + (m * j + i ) * s
4.3 特殊矩阵的压缩存储
特殊矩阵的定义:值相同元素或者零元素分布有一定规律的矩阵称。
4.3.1 对称矩阵的压缩存储
对称矩阵满足: a i j a_{i j} aij= a j i a_{j i} aji的n阶对称阵。
-
需要的内存空间
n(n + 1) / 2
个单元的空间
-
对称矩阵位置计算
以一维数组sa[ ]
作为n 阶对称矩阵A
的存储结构,A
中任意一元素 a i j a_{ij} aij与它的存储位置sa[k]
之间存在着如下对应关系:
4.3.2 三角矩阵的压缩存储
三角矩阵的定义:以主对角线划分,三角矩阵有上三角和下三角两种。
上三角矩阵,它的下三角(不包括主对角线)中的元素均为常数。下三角矩阵正好相反,它的主对角线上方均为常数。
在大多数情况下,三角矩阵常数为零。
-
需要的内存空间
n(n + 1) / 2+1
个单元的空间
-
下三角矩阵位置的计算
-
上三角矩阵位置的计算
第五章 树状结构
前几章讨论的问题都属于线性结构, 线性结构的特点是逻辑结构简单, 易于进行查找、插入和删除等操作。而现实生活中的许多事物的关系并非这样简单,如人类社会的族谱, 各种社会组织机构以及城市交通, 通讯等。这些事物中的联系都是非线性的,采用非线性结构进行描绘会更明确和便利。
非线性结构:在该结构中至少存在一个数据元素,有两个或两个以上的直接前驱(或直接后继)元素。树形结构和图形结构就是其中十分重要的非线性结构。
5.1 二叉树
二叉树(Binary Tree):是有限个元素的集合,该集合或者为空,或者由一个称为根(root)的元素及两个不相交的,分别称为左子树和右子树的二叉树组成。左、右子树本身也是二叉树。
- 两棵不同的二叉树
二叉树结点的子树要区分左子树和右子树, 即使只有一棵子树也要进行区分,说明它是左子树,还是右子树。
- 二叉树的五种基本形态
- 特殊的两种二叉树
① 满二叉树(Full Binary Tree):一棵深度为k且有2k - 1
个结点的二叉树。
② 完全二叉树(Complete Binary Tree): 若二叉树的深度为k,且共有n个结点。对树中结点按从上到下、从左到右的顺序进行编号,若编号为i(1≤i≤n)的结点与满二叉树编号为i的结点位置相同,则此树称为完全二叉树。
除第k层外,其它各层(1~k-1)的结点数都达到最大个数,第k层从右向左连续缺若干结点,这就是完全二叉树。
🔴注意:一棵满二叉树一定是一棵完全二叉树;但一棵完全二叉树不一定是满二叉树
5.2 二叉树的性质
性质1 : 若二叉树的层次从1开始, 则在二叉树的第 i 层最多有 2 i 2^i 2i-1个结点。
性质2 : 深度为k的二叉树最多有 2k - 1个结点。
性质3 :对任何一棵二叉树, 如果其叶结点个数为 n 0 n_0 n0,度为2的结点个数为 n 2 n_2 n2, 则有 n 0 n_0 n0= n 2 n_2 n2+1。
性质4 : 具有n个结点的完全二叉树的深度为 l o g 2 n log_2n log2n + 1
性质5 : 对于具有n个结点的完全二叉树,若从上至下和从左至右的顺序对二叉树中的所有结点从1开始编号,则对任意的结点i有:
(1) 若i > 1, 则其双亲为i / 2(整除),若i = 1, i无双亲结点。
(2) 若2i <= n, 则i的左孩子序号为2i, 若2i > n, 则i无左孩子。
(3) 若2i + 1 <= n, 则i的右孩子序号为2i + 1, 若2i + 1 > n, 则i无右孩子。(该性质可用数学归纳法证明)
5.3 二叉树的存储
5.3.1 二叉树的顺序存储结构及其优缺点
用一组连续的内存单元, 按编号顺序依次存储二叉树的元素。
①完全二叉树的顺序存储结构:
用一维数组 bt[ ]存放一棵完全二叉树, 将标号为i的结点的数据元素存放在分量bt[i-1]中。
②非完全二叉树的顺序存储结构:
按完全二叉树的形式补齐二叉树所缺少的那些结点,对二叉树结点编号, 将二叉树原有的结点按编号存储到内存单元“相应”的位置上。
代码实现:
#define MAXNODE 100
typedef int DataType;
typedef DataType SqBiTree[MAXNODE];
SqBiTree bt;
//这时,bt就可以保存MAXNODE个DataType类型的元素, bt[0]就可保存根结点。
//当然,bt也可以这样定义:
#define MAXNODE 100
DataType bt[MAXNODE];
- 二叉树的顺序存储结构的优缺点
虽然顺序存储结构比较适合存储完全二叉树, 但由于一般二叉树必须仿照完全二叉树那样存储,可能会浪费很多存储空间,单支树就是一个极端情况。
5.3.1 二叉树的链式存储结构
① 二叉链表:二叉链表中每个结点包含三个域:数据域、左孩子域、右孩子域。
代码实现
typedef struct BiTNode
{
datatype data;
struct BiTNode* lchild, * rchild;
} BiTNode, * BiTree;
②二叉树的三叉链表存储:三叉链表中每个结点包含四个域:数据域、双亲指针域、左孩子域、右孩子域。
代码实现:
typedef struct node
{
elemtype data;
struct node* Lchild, * Rchild, * parent;
} bitree;
5.2 二叉树的遍历
5.2.1 递归的三种遍历
由于二叉树由根、左子树、右子树三部分组成,遍历可以分解为:访问根,遍历左子树和遍历右子树。令:L:遍历左子树 D:访问根结点 R:遍历右子树,排列组合下来一共有6种方法。但是我们约定从左遍历,那么就有三种遍历方式:D L R
先序遍历、L D R
中序遍历、L R D
后序遍历。
① 先序遍历法:
void PreOrder(BiTree bt)
{
if (bt == NULL) return;
visit(bt->data);
PreOrder(bt->lchild);
PreOrder(bt->rchild);
}
- 正常思维:根左右
- 笨办法:可参考中序遍历的图
② 中序遍历法:
void PreOrder(BiTree bt)
{
if (bt == NULL) return;
PreOrder(bt->lchild);
visit(bt->data);
PreOrder(bt->rchild);
}
-
正常思维:左根右
-
笨办法:
-
nb办法:把树🌳压扁就是中序遍历!
③ 后序遍历法:
void PostOrder(BiTree bt)
{
if (bt == NULL) return;
PostOrder(bt->lchild);
PostOrder(bt->rchild);
Visit(bt->data);
}
- 正常思维:左右根
- 笨办法:参考中序遍历
5.2.2 层次遍历法
void LevelOrder(BiTree bt)
{
BiTree queue[MAXNODE];
int front, rear;
if (bt == NULL) return;
front = -1;
rear = 0;
queue[rear] = bt;//根结点入队
while (front != rear)//队列不为空,则循环
{
front++;
Visit(queue[front]->data);//出队列,并访问结点
if (queue[front]->lchild != NULL)//将队头结点的左孩子入队
{
rear++;
queue[rear] = queue[front]->lchild;
}
if (queue[front]->rchild != NULL)//将队头结点的右孩子入队
{
rear++;
queue[rear] = queue[front]->rchild;
}
}
}
5.3 二叉树遍历的应用
5.3.1 创建二叉树的二叉链表存储
void CreateBinTree(BiTree* T) /* 以递归方式建立二叉树 */
{
char ch;
scanf("\n%c", &ch);
if (ch == '0') *T = NULL; /* 以字符0作为空结点的值以结束递归 */
else {
*T = (BiTNode*)malloc(sizeof(BiTNode));
(*T)->data = ch;
CreateBinTree(&(*T)->lchild);
CreateBinTree(&(*T)->rchild);
}
}
5.3.2 由遍历序列恢复二叉树
例 已知前序序列{ ABHFDECKG } 和中序序列{ HBDFAEKCG }, 试构造该二叉树。
🔴注意:
给出先序和中序可以确定一棵二叉树,给出中序和后序也可以确定一棵二叉树, 但给出
先序和后序不能唯一地确定一棵二叉树。
5.3.3 在二叉树中查找值为 x的数据元素
BiTree Search(BiTree bt, datatype x)
{
BiTree p;
if (bt)
{
if (bt->data == x) return bt;
if (bt->Lchild != NULL)
{
p = Search(bt->lchild, x);
if (p) return p;
}
if (bt->rchild != NULL)
{
p = Search(bt->rchild, x);
if (p) return p;
}
}
return NULL;
}
5.3.4 统计给定二叉树中叶子结点的数目
int CountLeaf2(BiTree bt)
{
if (bt == NULL) return(0);
if (bt->lchild == NULL && bt->rchild == NULL) return(1);
return(CountLeaf2(bt->lchild) + CountLeaf2(bt->rchild));
}
5.4 哈夫曼树及哈夫曼编码
哈夫曼树:假设有n个权值(w1, w2, …, wn),构造有n个叶子结点的二叉树, 每个叶子结点有一个 w i w_i wi 作为它的权值,则带权路径长度最小的二叉树称为哈夫曼树(即 WPL最小的二叉树)。
🔵特点:
① 权值大的结点靠近根
② 没有度为1的结点
③ WPL最小
5.4.1 哈夫曼树的构造
5.4.2 哈夫曼编码
作用:
🔵特点:
🔴注意: