线性表数据结构
线性表
(参考严老师的数据结构C语言版)
知识点
要点:数据对象中除第一个元素以外的任何一个元素有唯一的前驱,除最后一个元素以外的每一个元素有唯一的后继元素。
线性表的存储结构
-
连续的存储空间(数组)
- 静态结构,动态操作
- 每一条记录只需要一个信息域
- 线性表的第i个数据元素ai的存储位置为:LOC(ai)=LOC(a1)+(i-1)*L
L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
newbase=(ElemType*)realloc(L.elem,(L.elemsize+LISTINCREMENT)*sizeof(ElemType));
- 顺序表中插入、删除元素时的平均移动元素次数的期望值
E i n s = 1 n + 1 ∑ i = 1 n + 1 ( n + 1 − i ) = n 2 E_{ins}=\frac{1}{n+1}\sum_{i=1}^{n+1}(n+1-i)=\frac{n}{2} Eins=n+11∑i=1n+1(n+1−i)=2n(n个元素n+1个可插入位置)
E d e l = 1 n ∑ i = 1 n ( n − i ) = n − 1 2 E_{del}=\frac{1}{n}\sum_{i=1}^{n}(n-i)=\frac{n-1}{2} Edel=n1∑i=1n(n−i)=2n−1
-
非连续的存储空间——指针(链表)
- 一条记录需要一个信息域、一个指针域
- 表头结点的作用(注意链表的实现是否定义了表头结点的存在)
- 头结点的指针相当于一个地址常量
-
游标、静态链表(连续静态的存储空间+动态管理思想)
例如:
线性链表的实现(c)
Tips
- 都在旁边注释了………………
#include <stdio.h>
typedef int ElemType; //此处可以根据需要修改嗷
typedef struct NODE
{
ElemType *elem;
int len;
struct NODE *next; // c中不能在定义的时候初始化为NULL,故记得使用时初始化 //在结构体定义自身类型的指针成员时要带struct(typedef还未生效)
}NODE;
typedef NODE* LIST;
typedef NODE* Position; //增加可读性
/*******************************************函数声明*************************************************/
//c中函数声明要放在调用的前面,教材中标准ADT操作有12个
LIST CreateList(); //初始化链表,InitList()
void DestroyList(LIST head);
void visit(ElemType *p);
void ClearList(LIST head);
int ListEmpty(LIST head);
int ListLength(LIST head);
void ListInsert(LIST head, int i, NODE* e);
NODE* ListDelete(LIST head, int i);
void Deleval(LIST head, NODE* e);
void ReverseList(LIST head);
LIST CreateList2();
int compare(ElemType *p, ElemType *q);
void visit(ElemType *p);
void InsertElem2(NODE *p, NODE *q);
/*ADT补充*/
void LocateElem(LIST head,int e,int compare());
int GetElme(LIST head,int e);
/**
*返回某元素前驱
*返回某元素后继
**/
/*******************************************函数定义*************************************************/
NODE * CreateList(); // 创建空的头节点
//创建头节点作为第一个结点的前驱,返回头节点的指针
NODE * CreateList()
{
LIST head;
head = (LIST)malloc(sizeof(NODE));//head是一个头结点
head->next = NULL;
head->len = 0;
return head;
}
//销毁线性表
void DestroyList(LIST head)
{
ClearList(head);
free(head); //即释放掉头结点的空间
}
//将表置为空表
void ClearList(LIST head)
{
NODE* p = NULL;
while (head->next != NULL)
{
p = head->next;
head->next = p->next;
free(p);
}
head->len = 0;
}
//是否为空表
int ListEmpty(LIST head)
{
if (head->len != 0)
{
return 0;
}
return 1;
}
//返回数据元素个数
int ListLength(LIST head)
{
return head->len;
}
//插入新数据元素
void ListInsert(LIST head,int i,NODE* e)
{
int j = 0;
NODE* p = head;
if (p->next != NULL && i <= head->len)
{
for (j = 0; j < i - 1; j++)
{
p = p->next;
}
e->next = p->next->next;
p->next = e;
}
}
void ListInsert2(Position p, int x)
{
NODE *q;
q = (NODE*)malloc(sizeof(NODE));
q->next = p ->next;
p->next = q;
*(q->elem) = x;
}
//删除第i个数据元素,并返回指向该节点的指针
NODE* ListDelete(LIST head, int i)
{
NODE* p = head,*q=NULL;
ElemType *e;
int j = 0;
if (p->next != NULL && i <= head->len)
{
for (j = 9; j < i - 1; j++)
{
p = p->next;
}
q = p->next;
p->next = q->next;
free(q);
head->len -= 1;
return q;
}
}
//例2-1 删除L中所有符合要求的元素
void Deleval(LIST head,NODE* e)
{
NODE* p = head,*q = NULL;
int i = 0;
for (i = 0; i < head->len; i++)
{
if (compare(p->next->elem, e->elem) == 0)
{
q = p->next;
p->next = q->next;
free(q);
}
}
}
//例2-2 两表归并
LIST mergeList(LIST La, LIST Lb)
{
NODE* pa = La->next, *pb = Lb->next,*pc = La;
LIST Lc = pc;
while (pa&&pb)
{
if (compare(pa->elem, pb->elem))
{
pc->next = pa;
pc = pa;
pa = pa->next;
}
else
{
pc->next = pb;
pc = pb;
pb = pb->next;
}
}
pc->next = pa ? pa : pb; // 判断余表接在哪里!!!caution!!!
free(Lb); //因Lc由La开始,是在La上插入的,故T(1),只释放Lb
return Lc;
}
//线性表反向
void Reversl(LIST head)
{
Position p, q;
if (head->next != NULL)
{
p = head->next;
q = p->next;
while (q != NULL)
{
p->next = head->next;
head->next = p;
p = q;
q = p->next;
}
p->next = head->next;
head->next = p;
}
fprintf(stderr, "ERROR!The list is empty!");
}
//逆序遍历链表
void ReverseList(LIST head)
{
if (head->next)
{
ReverseList(head->next);
}
visit(head->elem);
}
//升序排序
void AcsSequence(LIST head)
{
Position p;
Position q;
NODE* temp;
p = head->next;
if (!p)
{
while (p->next)
{
q = head;
if (compare(q->next->elem, p->next->elem)) //此处不能相等嗷
{
q = q->next;
}
temp = p->next;
p->next = temp->next;
InsertELem2(q, temp);
}
}
}
//建立有序链表
LIST CreateList2()
{
Position p, head,q;
int x;
head = (LIST)malloc(sizeof(NODE));
head->next = NULL;
scanf_s("%d", &x);
while (x != -999)
{
p = head;
while ((p->next != NULL) && compare(*(p->next->elem), x)) // 注意&&顺序
{
p = p->next;
}
q = (NODE*)malloc(sizeof(NODE));
*(q->elem) = x;
InsertElem2(p, q);
scanf_s("%d", &x);
}
return head;
}
//降序排序(与升序的比较条件相反)即可
/******************************************************************辅助函数**********************************************/
int compare(ElemType *p, ElemType *q)
{
return 1;
}
void visit(ElemType *p)
{
return;
}
void InsertElem2(NODE *p,NODE *q) //在节点p后插入单独节点q
{
q->next = p->next;
p->next = q;
}
算法
合并
- 顺序存储的合并算法时间复杂度为O(m+n),空间复杂度为O(m+n);
- 链式存储的合并算法时间复杂度同顺序存储,但空间复杂度为O(1);
- 合并算法是内部排序的一种方法,但是对外部排序来讲,这是唯一方法。
pc = pa?pa:pb;
当一个链表先被排序完后,另一个表剩下的要接到新链表表尾。while((p->next != NULL)&&(p->next->data <= x))
用于判断插入,注意&&两个判断顺序不能换,否则会出错。(注意&&的性质)
双指针
- 单向链表环的问题(如何判断一个单向链表有环?找到环的入口节点)
判断一个单向链表是否有环:
在链表开头处设置快指针p,慢指针q。快指针步幅2,慢指针步幅1.则快指针相对于慢指针的速度是1.故如果有环,则两者一定会碰到(而不会有正好跳过之类的与奇数偶数相关的奇怪的问题)。
如何找到入口节点嘞?
考虑判断是否有环时p、q相遇于meet,设其据入口inter处的距离为d,起点据meet长度为x。则当慢指针q刚到达inter时,快指针在其前进方向的后面落后q正好d个结点(如此相对于q,快指针每次追1个结点走d个结点与q相遇)。则在两者相遇于meet时,快指针p路程为2x,x为圈长n的整数倍。此时在起点处再放置指针r步幅1,令r与慢指针q均以步幅1的速度前行,则当r走了(x-d)即位于inter处时,慢指针q也正好至inter处,两指针相遇于入口。 - 线性链表,求倒数第k个数
设置前后两个指针,当前面的指针走到前序第k个的时候后面的也开始走,指导前面的指针走到结尾,此时后面的指针及所求。 - 线性链表,求中间位置的元素
快慢指针,一个1一个2。 - 单向链表交叉问题
快慢指针,从“尾对齐”
思路:快慢指针(步长and起始点)
- 两个指针一快一慢,判断是否会重叠。
- 两个指针,一个只在正序K位,一个在头。步长均为一。当快指针走到尾时,慢指针走到l-k位即倒数第K个数
- 两个指针,一个步长为一,一个步长为二。当快指针指向尾时,慢指针指向中点。
- 第一遍遍历,判断尾地址是否相等,同时得到两个链表的长度l1,l2。令较长表的指针从长度差值的节点开始,另一个从头开始,当相等时即为交叉点。
链表存在的问题
- 每创建一个节点,都要进行一次系统调用分配空间,浪费时间
- 创建节点时间不固定,导致节点分配空间不连续,容易形成离散内存碎片
- 内存不连续,局部访问性差,容易出现cache(告诉缓冲存储器)缺失
对链表的改进——内存池
- 类似静态链表
- 单线程,只支持固定块大小的内存池,分配N块内存池,每需要一个节点就从内存池中申请空闲的块。
线性表的游标实现
已经不用了但是要注重思想
节点类型
struct Spacestr{
ElemType data;
int next;
}
以数组作为整个操作空间,index充当地址进行链式操作
其它类型链表
循环链表
- 表中最后一个结点的指针域指向头结点
- 头指针指向头结点或指向尾结点(操作简化)
- 循环条件为p是否等于头指针
- 链表为空时,只有一个头结点,其尾结点指向自己
双向链表(DLIST)
- 结点类型
有DnodeType *next,*previous;
- 循环链表则有两个环
- 为空时头结点的pre、next均指向自己
- 若未设头指针,则设置尾指针,则R->next为首元结点
- 空双向链表:
R = NULL;
- 单一节点的双向链表:
- 空双向链表:
R->next = R;
R ->previous = R;
链表的ADT操作
- 由于链表在空间的合理利用、插入、删除不需要移动等优点,在很多场合下都是首选的存储结构。
- 位序 的概念已经淡化故有其自适的ADT(教材上有22个)
ADT
typedef struct LNode{//结点类型
ElemType data;
struct LNode *next;
}*Link,*Positon;
typedef struct{//链表类型
Link head,tail;//头指针指向头结点,尾指针
int len; //链表中的数据元素个数
}LinkList;
/*ADT基本操作*/
Status MakeNode(Link &p,Elemtype e);//分配由p指向值为e的结点
void FreeNode(Link &p);//释放p结点
//InitList
//DestroyList
//ClearList
Status InsFirst(Link h,Link s);//已知h指向头结点,在首元结点前插入s
Status DelFirst(Link h,Link &q);//删除首元结点以q返回
Status Append(Link &L,Link s);//将指针s所指的链表链接在L的最后一个节点之后,并改变链表L的尾指针指向新的尾指针结点
Staus Remove(LinkList &L,Link &q);//删除L的尾结点并以q返回,改变L的尾指针指向的结点
Staus InsBefore(LinkList &L,Link &p,Link s);//将s所指结点插入在p所指结点之前,并修改p指针指向新插入的结点,修改链表长度指。
Staus InsAfter(LinkList &L,Link &p,Link s);//要注意判断尾指针是否需要更新
//其它的与顺序存储基本一致
部分实现
//在表左端插入节点
void DLInsert(Elemtype *x,LIST R)
{
NODE *p;
p = (ElemType*)malloc(sizeof(ElemType*));
p ->elem = x;
if(R = NULL) // 不要忽略R是空表的情况
{
R = p;
p -next = p;
}
else
{
p -> next = R->next;
R -next = p;
}
};
//在表右端插入节点
void DRInsert(ElemType *x,LIST R)
{
LInsert(x,R);
R = R->next; //环形链表实际上无左右之分,只是移动R之差别
};
//从表左端删除结点
void LDelete(LIST R)
{
NODE* p;
if(R = NULL)
{
printf("ERROR!"); // 不要忽略R为空表的情况
}
else
{
p = R->next;
R-next = p-next;
if(p == R)
{R = NULL} //不要忽略只有一个结点,删去后为空表的情况
free(p);
}
};
//单向环形链表反向
void Revers(LIST R)
{
Position p,q;
p = R->next;
R -> next = p->next;
p ->next = p;
while(R ->next != R) // 剩下单独一个节点做单独处理
{
q = R->next;
R->next = q->next;
q->next = p->next;
p->next = q;
}
R->next = p ->next;
p->next = R;
R = p;
};