数据结构
1.基本概念和术语
2.逻辑结构和物理结构
逻辑结构:集合结构、线性结构、树形结构、图形结构
物理结构:顺序存储结构、链式存储结构
3.抽象数据结构类型
算法
算法是在数据结构的基础上进行实现的。
定义:算法是解决特定问题的求解步骤的描述。在计算机中表现为指令的有限序列。并且每个指令表示一个或多个操作。
1.算法的特性
算法的五大特性:输入、输出、有穷性、确定性、可行性。
输入:可以有0个或多个输入
输出:至少有一个或多个输出
有穷性:算法在指令有限的步骤后,自动结束而不会出现无限循环,并且每个步骤在可接受的时间内完成。
确定性:算法的每一步 都具有名曲的含义,不会出现二义性。
可行性:算法的每一步都是可行的
2.算法设计的要求
算法设计的要求:正确性、可读性、健壮性、时间效率高和存储量低
正确性:算法至少具有输入、输出和加工处理无歧义性,能反应问题的需求、能够得到问题的正确答案。
可读性:为了便于阅读、理解和交流。
健壮性:当输入不合法时,算法也能能作出相关处理,而不是产生异常或莫名其妙的结果。
时间效率高和存储量低:尽量满足时间效率高和存储量低的需求。
3.算法效率度量方法
算法效率的度量方法:事后统计法、事前分析估算方法
一个高级程序语言编写的程序在计算机上运行时所消耗的时间取决于以下因素:
1.算法采用的策略、方法。
2.编译产生的代码质量。
3.问题输入规模。
4.机器指令的执行速度。
一个程序的运行时间依赖于算法的好坏和问题的输入规模
4.函数的渐进增长
我们在分析一个算法的运行时间时,重要的是把基本操作的是数量和输入规模关联起来。
判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,二更应该关注主项(最高阶项)的阶数
5.时间复杂度
大O记法
T(n)增长最慢的算法为最优算法
推导大O阶方法:
1.用常数1取代运行时间的所有加法常数。
2.在修改后的运行次数函数中,只保留最高阶
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。
得到的结果就是大O阶
常见的时间复杂度:
函数调用的时间复杂度分析
并列加,包含乘
最坏情况和平均情况
一般指最坏时间复杂度
6.空间复杂度
线性表
定义:零个或多个数据元素的有限序列。
若有多个元素,则第一个元素无前驱,最后一个元素无后继。其他每个元素都有且只有一个前驱和后继。
1.抽象数据类型
2.顺序存储实现
线性表对一个数组进行封装
代码描述:
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
/* data */
ElemType data[MAXSIZE];
int length;
}Sqlist;
顺序存储结构封装的三个属性:存储空间的起始位置、线性表的最大存储容量、线性表当前长度。
地址计算方法:Loc(ai) = Loc(a1) + (i -1)*c 注:c表示ElemType的存储单元(字节)。
获得元素
思路:将线性表L中的第i个位置元素值返回。
//1.获得元素操作
/*
Status是函数的类型,其值是函数结果的状态代码,如OK等
初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
操作结果:用e返回L中第i个数据元素的值
*/
Status GetElem(Sqlist L,int i,ElemType *e){
if(L.length == 0 || i < 1 || i > L.length){
return ERROR;
}
*e = L.data[i - 1];
return OK;
}
插入元素
思路: 如果插入位置不合理,抛出异常
如果线性表的长度大于等于数组长度,抛出异常异常或动态 增加容量。
从最后一个元素开始向前表里到第i个位置,分别将他们都向后移动一个位置。
将要插入的元素填入位置i处
表长加1.
//2.插入操作
/*
初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
操作结果:在L中第i个位置之前插入新的数据元素,L的长度加1
*/
Status ListInsert(Sqlist *L,int i,ElemType e){
int k;
if (L->length == MAXSIZE)/*顺序线性表已经满了*/
{
return ERROR;
}
if (i < 1 || i > L->length + 1)/*当i不在范围内时*/
{
return ERROR;
}
if (i <= L->length)/*若插入书籍位置不在表位*/
{
for (k = L -> length - 1;k >= i - 1;k--)/*将要插入位置后数据元素向后移一位*/
{
L->data[k+1] = L->data[k];
}
}
L->data[i -1] = e;
L->length++;
return OK;
}
删除操作
思路: 如果删除的位置不合理,跑出异常
取出删除元素
从删除元素位置开始遍历到按最后一个元素位置,分别将他们都向前移动一个位置
表长减一。
//3.删除操作
/*
初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
操作结果:删除L中第i个数据元素,并用e返回其值,L的长度减1
*/
Status ListDelete(Sqlist *L,int i,ElemType *e){
int k;
if (L->length == 0)/*线性表为空*/
{
return ERROR;
}
if(i < 0 || i > L->length)/*删除位置不正确*/
{
return ERROR;
}
*e = L->data[i - 1]; /*记录要删除的元素*/
if (i < L->length)
{
for(k = i; k < L->length;k++)/*简化删除位置后继元素前移*/
{
L->data[k -1] = L->data[k];
}
}
L->length--;
return OK;
}
总结:线性表的顺序存储结构,在存、读数据时时间复杂度是O(1)
而插入和删除时,时间复杂度都是O(n)
3.链式存储实现
用一组任意的存储单元存储线性表的数据元素
链式存储结构中每一个结点除了存储数据元素信息外,还要存储它的后继元素的存储地址。
存储数据元素信息的域叫数据域
存储直接后继位置的域叫指针域,存储的信息叫指针或链
每个结点自包含一个指针域的叫作单链表
头指针:链表中的第一个结点的存储位置
头结点:单链表的第一个结点前附设一个结点,用于存储的附加信息。
代码描述:
/*线性表单链表存储结构*/
typedef int ElemType;
typedef struct Node
{
/* data */
ElemType data;
struct Node *next;
} Node;
typedef struct Node * LinkList;/*定义LinkList,将struct Node *重命名为LinkList*/
单链表读取
思路:
1.声明一个结点p指向链表的第一个结点,初始化j从1开始。
2.当j<i时,就是遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
3.若到链表末尾p为空,则说明第i个元素不存在,
4.否则查找成功,返回结点p的数据
时间复杂度O(n)
//1.获取元素
/*
初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
操作结果:用e返回L中第i个数据元素的值
*/
Status GetElem(LinkList L,int i,ElemType *e){
int j;
LinkList p;/*声明一节点p*/
p = L->next;/*让p指向链表L的第一个节点*/
j = 1; /*j为计数器*/
while (p && j < i)/*p不为空或者计数器还没有等于i,循环继续*/
{
p = p->next;/*让结点指向下一个节点*/
++j;
}
if (!p || j > i)
{
return ERROR; /*第i个元素不存在*/
}
*e = p->data;/*取第i个元素的数据*/
return OK;
}
单链表插入
思路:
1.声明一个结点p指向链表的第一个结点,初始化j从1开始。
2.当j<i时,就是遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
3.若到链表末尾p为空,则说明第i个元素不存在,
4.否则查找成功,在系统中生成一个空结点s
5.将数据元素e赋值给s->data
6.单链表的插入标准语句是s->next = p->next; p->next = s;
7.返回成功
时间复杂度O(1)
//2.插入元素
/*
初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
操作结果:在L中第i个位置之前插入新的数据元素,L的长度加1
*/
Status ListInsert(LinkList *L,int i,ElemType e){
int j;
LinkList p,s;
p = *L;
j = 1;
while (p && j < i) /*寻找第i个结点*/
{
p = p->next;
++j;
}
if (!p || j > i)
{
return ERROR;/*第i个元素不存在*/
}
s = (LinkList)malloc(sizeof(Node));/*生成C结点*/
s->data = e;
s->next = p->next;/*将p的后继结点赋值给s的后继*/
p->next = s;/*将s赋值给p的后继*/
return OK;
}
单链表删除
思路:
1.声明一个结点p指向链表的第一个结点,初始化j从1开始。
2.当j<i时,就是遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
3.若到链表末尾p为空,则说明第i个元素不存在,
4.否则查找成功,将欲删除的结点p->next赋值给q
5.单链表的删除标准语句是p->next = q->next;
6.将q结点中的数据赋值给e,作为返回
7.释放q结点
8.返回成功
时间复杂度O(1)
//3.删除操作
/*
初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
操作结果:删除L中第i个数据元素,并用e返回其值,L的长度减1
*/
Status ListDelete(LinkList *L, int i,ElemType *e){
int j;
LinkList p,q;
p = *L;
j = 1;
while (p->next && j < i) /*寻找第i个结点*/
{
p = p->next;
++j;
}
if (!(p->next) || j > i)/*第i个元素不存在*/
{
return ERROR;
}
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
单链表整表创建
思路:
1.声明一个结点p和计数器变量i;
2.初始化一个空链表L;
3.让L的头结点的指针指向null,即建立一个带头结点的单链表
4.循环:
生成一新结点赋值给p;
随机生成一数字赋值给p的数据域p->data
将p插入到头结点与前一新节点之间
代码描述:
//4.创建整单链表
/*随机产生n个元素的值,建立带表头结点的单链表线性表L(头插法)*/
void createListHead(LinkList *L,int n){
LinkList p;
int i;
srand(time(0));/*初始化随机种子*/
*L = (LinkList)malloc(sizeof(Node));/*返回值为void*型*/
(*L)->next = NULL;/*先建立一个带头结点的单链表*/
for ( i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(Node));/*生成新节点*/
p->data = rand()%100+1;
p->next = (*L)->next;
(*L)->next = p;/*插入到表头*/
}
}
/*随机产生n个元素的值,建立带表头结点的单链表线性表L(头插法)*/
void createListTail(LinkList *L,int n){
LinkList p,r;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));//*L相当于头指针
r = *L;/*r为指向尾部的节点*/
for ( i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p;
}
r->next = NULL;
}
单链表整表删除
思路:
1.声明一结点p和q
2.将第一个结点赋值给p
3.循环:
将下一结点赋值给q
释放p
将q赋值给p
代码描述:
//5.单链表的整表删除
/*初始条件,顺序线性表L已存在,
操作结果:将L重置为空表
*/
Status ClearList(LinkList *L){
LinkList p,q;
p =(*L)->next;/*p指向第一个结点*/
while (p)
{
q = p->next;
free(q);
p = q;
}
(*L)->next = NULL;
return OK;
}
单链表结构与顺序存储结构的优缺点
- 存储分配方式
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的数据元素
- 时间性能
- 查找
顺序存储结构O(1)
单链表O(n) - 插入和删除
顺序存储结构需要平均移动表长一半的元素,时间为O(n)
单链表在找出某位置的指针后,插入和删除时间为O(1)
- 查找
- 空间性能
- 顺序需要预分配存储空间,分大了,浪费,分小了,容易溢出
- 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制
若需要查找比较多,插入和删除较少,则适合用顺序存储结构,反之适合单链表结构。
若不确定元素个数时,适合采用单链表,反之用顺序存储结构
4.静态链表
用数组描述得链表
对数组的第一个和最后一个元素作特殊元素处理,不存数据。把未被使用的数组元素称为备用链表。
存储结构:
ypedef int ElemType;
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur;/*游标(cursor),为0是表示无指向*/
}Component,StaticLinkList[MAXSIZE];
初始化:
/*将一位数组sapce中各分量链成一个被用链表*/
Status InitList(StaticLinkList space){
int i;
for(i = 0;i < MAXSIZE - 1;i++){
space[i].cur = i + 1;
}
space[MAXSIZE - 1].cur = 0;/*目前静态链表为空,最后以个元素的cur为0*/
return OK;
}
插入操作
思路:
每次进行插入时,可以从备用链表上拿到第一个结点作为待插入的新结点。
代码描述:
数组空间分配函数:
/*若备用空间链表非空,则返回分配节点的下标,否则返回0*/
int Malloc_SLL(StaticLinkList space){
int i = space[0].cur;/*当前数组的第一个元素的cur存的值*/
/* 就是要返回的第一个备用空闲的下标*/
if (space[0].cur)
{
space[0].cur = space[i].cur;/*由于要拿出一个分量来使用了所以我们
就得把它的下一个分量用来做备用*/
}
return i;
}
插入操作
/*在L中第i个元素之前插入新的数据元素e*/
Status ListInsert(StaticLinkList L,int i,ElemType e){
int j,k,l;
k = MAXSIZE - 1;/*注意k首先是最后一个元素的下标*/
if(i < 1 || i > ListLength(L) + 1){
return ERROR;
}
j = Malloc_SLL(L);/*获得空闲分量的下标s*/
if(j){
L[j].data = e;/*将数值赋值给次分量的data*/
for ( l = 1; l <= i - 1; l++)
{
k = L[k].cur;/*找到第i个元素之前的位置*/
}
L[j].cur = L[k].cur;/*把第i个元素之前的cur赋值给新元素的cur*/
L[k].cur = j;/*把新元素的下标赋值给第i个元素之前的cur*/
return OK;
}
return ERROR;
}
删除操作
思路:
代码描述:
数组空间释放函数:
/*将下标为k的空闲节点回收到备用链表*/
void Free_SSL(StaticLinkList space,int k){
space[k].cur = space[0].cur;/*把第一个元素cur值赋值给要删除的元素*/
space[0].cur = k;/*把要删除的分量下标赋值给第一个元素cur*/
}
删除操作
/*删除在L中第i个数据元素e*/
Status ListDele(StaticLinkList L,int i){
int j,k;
if(i < 1 || i > ListLength(L)){
return ERROR;
}
k = MAXSIZE - 1;
for ( j = 1; j <= i - 1; j++)
{
k = L[k].cur;
}
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L,j);
return OK;
}
链表长
/*初始条件:静态链表L已存在,操作结果:返回L中数据元素个数*/
int ListLength(StaticLinkList L){
int j = 0;
int i = L[MAXSIZE - 1].cur;//指向第一个元素下标
while (i)
{
i = L[i].cur;
j++;
}
return j;
}
优缺点
- 优点
在插入和删除时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中插入和删除操作需要移动大量元素的缺点 - 缺点
没有解决连续存储分配带来的表长度难以确定的问题
失去了顺序存储结构的随机存储特性
快速找到位置长度单链表的中间结点
1.普通方法:
首先遍历单链表得到单链表的长度L,再从头结点除法循环L/2次得到单链表的中间节点。
复杂度为:O(L+l/2)=O(3L/2)
2.快慢指针:
设置两个指针search、mid都指向单链表的头结点。其中search的移动速度是mid的两倍,当*search指向末尾节点的时候,mid正好就在中间了。
代码实现:
Status GetMidNode(LinkList L,ElemType *e){
LinkList search,mid;
mid = search = L;
while(search->next->next !=NULL){
if(search->next->next !=NULL){
search = search->next->next;
mid = mid->next;
}
else{
search = search->next;
}
}
*e = mid->data;
return OK;
}
5.循环链表
将单链表中的终端结点的指针由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表叫单循环链表
原来是判断p->next是否为空,现在是判断p->next不等于头结点,则循环未结束。
两个循环链表合并:
p = rearA->next;/*保存A表的头结点*/
rearA->next = rearB->next->next;/*将本是指向B表第一个结点,赋值给rearA->next*/
rearB->next = p;/*将原来A表的头结点赋值给rearB->next*/
free(p);/*释放p*/
约瑟夫问题:
一个圈共有N个人(N为不确定的数字),第一个人的编号为0或者1,假设这边我将第一个人的编号设置为1号,那么第二个人的编号就为2号,第三个人的编号就为3号,第N个人的编号就为N号,现在提供一个数字M,第一个人开始从1报数,第二个人报的数就是2,依次类推,报到M这个数字的人出局,紧接着从出局的这个人的下一个人重新开始从1报数,和上面过程类似,报到M的人出局,直到N个人全部出局,请问,这个出局的顺序是什么?
代码实现(循环链表):
约瑟夫问题进阶:
进阶约瑟夫问题: 编号为1~n的n个人按顺序成一圈, 每个人持有一个密码m(密码为正整数),
从第1个人开始报数, 数到m的人出局, 且将他的密码作为新的出局号码,
下1个人重新从1开始报数,持续此过程, 直至最后1人
判断单链表是否有环:
方法一:使用p、q两个指针,p总是向前走,但q每次都从头开始走,对于每个节点,看p走的步数是否和q一样。若步数不等,出现矛盾,存在环。
方法二:使用p、q两个指针,p每次向前走一步,q每次向前走两步,若在某个时候p == q,则存在环。
魔术师发牌问题
问题描述:魔术师利用一副牌中的13张黑牌,预先将他们排好后叠放在一起,牌面朝下。对观众说:“我不看牌,只数数就可以猜到每张牌是什么,我大声数数,你们听,不信?现场演示。”魔术师将最上面的那张牌数为1,把他翻过来正好是黑桃A,将黑桃A放在桌子上,第二次数1,2,将第一张牌放在这些牌的下面,将第二张牌翻过来,正好是黑桃2,也将它放在桌子上这样依次进行将13张牌全部翻出,准确无误。
问题:牌的开始顺序是如何安排的?
拉丁矩阵
拉丁方阵是一种n×n的方阵,方阵中恰有n种不同的元素,每种元素恰有n个,并且每种元素在一行和一列中 恰好出现一次。著名数学家和物理学家欧拉使用拉丁字母来作为拉丁方阵里元素的符号,拉丁方阵因此而得名。
6.双向链表
双向链表就使在单链表的基础上,在每个结点中在设置一个指向其前驱结点的指针域,也就是说双链表的每个节点都有两个指针域,一个指向直接后继一个指向直接前驱。
双链表存储结构
typedef struct DulNode
{
/* data */
ElemType data;
struct Node *prior;/*直接前驱指针*/
struct Node *next;/*直接后继指针*/
} DulNode,*DulLinkList;
在一个双向链表中,对于链表中的一个结点p, 它的后继的前驱是它本身,它的前驱的后继也是它自己。即:
p->next->prior = p = p->prior->next
在插入和删除数据时需要修改两个指针变量
插入操作:
s->prior = p;/*把p赋值给s的前驱,如1*/
s->next = p->next;/*把p->next赋值给s的后继如2*/
p->next->prior = s;/*把s赋值给p->next的前驱如3*/
p->next = s;/*把s赋值给p的后继如4*/
顺序是先搞定s的前驱和后继,再搞定后结点的前驱,最后解决前结点的后继。
删除操作:
p->prior->next = p->next;/*把p->next赋值给p->prior的后继如1*/
p->next->prior = p->prior;/*把p->prior赋值给p->next的前驱如2*/
free(p);/*释放结点*/
凯撒和维吉尼亚加密算法
凯撒加密算法:
维吉尼亚加密算法: