一.算法
1.推导大O阶方法:
2.常见的时间复杂度:
3.最坏情况与平均情况
4.算法的空间复杂度
二.线性表
1.线性表定义:
由0个或多个数据元素组成的有限序列。
注意:
- 他是一个序列;
- 若存在多个元素,则第一个元素无前驱,最后一个元素无后继,其他元素都有且只有一个前驱和后继;
- 线性表强调有限的。
2.线性表的顺序存储结构:
结构代码:
#include <stdio.h>
#define MAXSZIE 20;
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int length; //线性表当前的长度
}SqList;
顺序存储结构封装需要的三个属性:
- 存储空间的起始位置:数组data,他的存储位置就是线性表存储空间的存储位置;
- 线性表的最大存储容量:数组的长度MAXSIZE(是存放线性表的存储空间的总长度,初始化后不会变);
- 线性表的当前长度:length(是线性表的元素个数,是会变化的)。
地址的计算方法:
1.线性表从1开始,回归正常思维;
2.
2.1.线性表顺序存储结构获得数组元素
#define OK 1;
#define ERROR 0;
#define TRUE 1;
#define FALSE 0;
typedef int Status;
//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 ;
}
2.2.线性表顺序存储结构插入操作
插入算法思路:
-
如果插入位置不合理,退出程序;
-
如果线性表长度>=数组长度,则退出程序or动态增加数组容量;
-
从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
-
将要插入元素填入位置i处;
-
线性表长+1。
代码实现:
Statua 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; k >= i - 1; k--)
{
L->data[k+1] = L->data[k];
}
}
L->data[i-1] = e; //将新元素插入
L->length++;
return OK;
}
2.3.线性表顺序存储结构删除操作
删除算法思路:
-
如果删除位置不合理,退出程序;
-
取出删除元素;
-
从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置;
-
表长-1。
代码实现:
Status ListDelete(Sqlist *L,int i,ElemType *e) { int k; if(L->length == 0) //线性表为空 { return ERROR; } if(i < 1 || 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; }
2.4.线性表顺序存储结构的优缺点:
-
结构在存、读数据时,时间复杂度都是O(1);
而在插入或删除时,时间复杂度都是O(n)。
-
这种结构比较适合元素个数比较稳定,不轻易插入和删除元素,更多的操作是存取数据的应用。
-
3.线性表静态链表:
用数组描述的链表,称之为静态链表。这种描述方法叫做游标实现法。
最后一个游标指向第一个有数据元素的下标;
第一个游标指向第一个没数据的下标。
除此之外,每个元素的游标都指向下一个元素的下标。
3.1静态链表存储结构:
#define MAXSIZE 1000
typedef struct{
ElemType data; //数据
int cur; //游标(Cursor)
}Component, StaticLinkList[MAXSIZE];
3.2静态链表的初始化:
相当于初始化数组。
Status IntLinst(StaticLinkList space)
{
int i;
for(i = 0;i < MAXSIZE - 1;i++)
{
space[i].cur= i + 1; //游标指向下一个元素下标
}
space[MAXSIZE-1].cur = 0; //最后一个游标指向0
return OK;
}
注意事项:
- 数组的第一个和最后一个元素做特殊处理,他们的data不存放数据。
- 通常把未使用的数组元素称为备用链表。
- 数组的第一个元素(下标为0的元素的cur),则存放备用链表的第一个结点的下标。
- 数组的最后一个元素(下标为MAXSIZE-1的cur),则存放第一个有数值的元素的下标,相当于单链表的头结点作用。
4.循环链表
优点:从表中任一结点出发均可找到表中其他结点。
**注意:**1.循环链表要想表示空,不是判断p或者p->next是否为空,而是判断他们是否等于头指针。
4.1 头尾指针表示单循环链表
4.1.1:头指针表示单循环链表
要找的结点 | 时间复杂度 |
---|---|
a1 | O(1) |
an | O(n) |
由此我们可以发现,头指针表示循环链表,非常不方便。
注意:表的操作常常是在表的首尾位置上进行。
4.1.2:尾指针表示单循环链表
要找的结点 | 该结点储存位置 | 时间复杂度 |
---|---|---|
a1 | R->next->next | O(1) |
an | R | O(1) |
说明: R->next是头结点。
4.2:合并带尾指针的循环链表
1.p存Ta表头结点
p = Ta->next;
2. 将Tb表头连接到Ta表尾巴
3.释放Tb表头结点
4.修改指针
合并小结:
代码实现:
LinkList Connect(LinkList Ta,Linklist Tb)//假设Ta.Tb都是非空的单循环链表
{
LinkList p = Ta->next; //p保存头结点
Ta->next = Tb->next->next; //Tb表头保存Ta表尾
free(Tb->next); //释放Tb表头结点
Tb->next = p; // 修改指针,b尾指回a头
return Tb;
}
5.双向链表
双向链表:在单链表的每个结点里面再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表。
5.1:双向链表的结构可定义如下:
代码如下:
typedef struct DulNode{
Elemtype data;
struct DulNode *prior,*next;
}DulNode,*DuLinkList;
5.2:双循环链表
-
让头结点的前驱指针指向链表的最后一个结点;
-
让最后一个结点的后继指针指向头结点。
如图:
5.3:双向链表结构的对称性(设指针p指向某一结点)
p->prior->next = p = p->next->prior
注意:双向链表中的有些操作(例如:获取链表的长度、获取链表的元素等),因为仅涉及一个方向的指针,故他们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者操作的时间复杂度均为O(n)。
5.4:双向链表的插入
思路:
1.s->prior = p->prior;
2.p->prior->next = s;
3.s->next = p;
4.p->prior = s;
代码如下:
void ListInsert_Dul(DuLinkList &L,int i,ElemType e)
{
//在带有头结点的双向循环链表L中第i个位置之前插入元素e
if(!( p = GetElemP_DUL(L,i))) //位置不合理,得到空指针
{
return ERROR;
}
s = newDULNode; //找到新节点
s->data = e; //赋值
s->prior = p->prior;
p->prior->next = s;
s->next = p;
p->prior = s;
return OK;
}
5.5:双向链表的删除
思路:
1.p->prior->next = p->next;
2.p->next->prior = p->prior;
代码如下:
void ListDelete_DuL(DuLinklist &L,int i,ElemType &e)
{
//删除带头结点的双向链表 L的第i个元素,并用e返回
if(!( p = GetElemP_DuL(L,i)))
{
return ERROR;
}
e = p->data;
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
return OK;
}
三种链表的时间效率比较:
链式存储结构的优缺点:
链式存储结构的优点 | 链式存储结构的缺点 |
---|---|
节点空间可以动态申请和释放 | 存储密度小,每个节点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大 |
数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素 | 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的 |
6.顺序表和链表的比较
7.线性表的应用
7.1:线性表的合并
问题描述:
假设利用两个线性表La和Lb分别表示两个集合A和B,先要求一个新的集合A=AUB
La = (7,5,3,11) Lb = (2,6,3) ----> La = (7,5,3,11,2,6)
算法步骤:
依次取出Lb中的每个元素,执行一下操作:
1.在La中查找该元素;
2.如果找不到,则将其插入La的最后。
代码实现
void whole(List &La,List Lb)
{
La_len = ListLength(La);
Lb_len = ListLength(Lb);
for(i = 1;i <= Lb_Length;i++)
{
FetElem(Lb,i,e);
if(!LocateElem(La,e))
{
ListInsert(&La,++La_lenth,e);
}
}
}
*算法的时间复杂度是:O(ListLength(La)ListLength(Lb)
7.2:有序表的合并
问题描述:
已知线性表La和Lb中的数据元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列。
La = (1,7,8)Lb = (2,4,6,8,10,11) ----> Lc = (1,2,4,6,7,8,8,10,11)
算法步骤:
- 创建一个空表Lc;
- 依次从La或Lb中“摘取“元素值较小的结点插入到Lc表的最后,直至其中一个表变空表为止;
- 继续将La或Lb其中一个表的剩余结点插入在Lc表的最后。
实现方法:
- 顺序表实现
void MergeList_Sq(SqList LA,SqList LB,SqList &LC)
{
pa = LA.elem;
pb = LB.elem; //指针pa和pb的初值为别指向两个表的第一个元素
LC.length = LA.length + LB.length; //新表长度为带合并两表的长度之和
LC.elem = new ElemType[LC.length]; //为合并后的新表分配一个数组空间
pc = LC.elem; //指针pc指向新表的第一个元素
pa_last = LA.elem + LA.length - 1; //指针pa_last指向LA表的最后一个元素
pb_last = LB.elem + LB.length - 1; //指针pb_last指向LB表的最后一个元素
while(pa <= pa_last && pb <= pa_last) //两个表都非空
{
if(*pa <= *pb) //依次"摘取"两表中较小的结点
{
*pc++ = *pa++;
}
else
{
*pc++ = *pb++;
}
}
while(pa <= pa_last)
{
*pc++ = *pa++; //LB表已到达表尾,将LA中剩余元素加入LC
}
while(pb <= pb_last)
{
*pc++ = *pb++; //LA表已到达表尾,将LB中剩余元素加入LC
}
}
算法时间和空间复杂度都是:O(ListLength(La) + ListLength(Lb))
- 用链表实现
以此类推…
接下来 插入剩余段:
合并后:
代码实现:
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc)
{
pa = La->next;
pb = Lb->next;
pc = Lc = La; //用La的头结点作为Lc的头结点
while(pa && pb)
{
if(pa->data <= pb->data)
{
pc->next = pa;
pc = pa;
pa = pa->next;
}
else
{
pc->next = pb;
pc = pb;
pb = pb->next;
}
}
if(pa) //删除剩余段
{
pc->next = pa;
}
else
{
pc->next = pb;
}
free(Lb); //释放Lb的头结点
}
算法的时间复杂度:O(ListLength(La) + ListLength(Lb))
算法的空间复杂度:O(1)
声明:此篇文章是自己在学习同时整理归纳的笔记,参考了B站小甲鱼和王卓老师的视频,图片也是从视频中截取了需要的部分。
王卓老师:https://www.bilibili.com/video/BV1nJ411V7bd?p=48
小甲鱼:https://www.bilibili.com/video/BV1jW411K7yg?p=9