一、线性表分类
存储方式:顺序存储——顺序表;链式存储——单链表、双链表、循环链表和静态链表
1、顺序表
逻辑上相邻、物理位置也相邻
顺序表的存储保密度高,每个节点只存储数据元素;
线性表的存储结构是一种随机存取的存储结构;
一个顺序表的所占用的存储空间大小与元素的存放顺序无关,与表的长度、元素的类型和元素各字段的类型有关;
线性表中元素的魏旭需
1.1 一维数组可以静态分配和动态分配
注意:动态分配并不是链式存储,属于顺序存储结构。分配的空间h大小可以在运行时决定
1.2 顺序表上的基本操作
(1)插入操作(1~n+1)
最好情况:在表尾插入,元素不在往后移动,时间复杂度O(1);
最坏的情况:在表头插入,元素后移将执行n次,时间复杂度O(n);
平均情况:随机插入,时间复杂度O(n);
因此,线性表插入算法的平均时间复杂度为O(n)。
(2)删除操作(1~n)
线性表删除算法的平均时间复杂度为O(n)。
(3)按值查找
线性表按值查找算法的平均时间复杂度为O(n)。
2.链表
不需要使用地址连续的存储单元
2.1单链表
线性表的链式存储;
链表节点除存放元素自身的信息外,还存放一个指向其后续的指针。
通常用头指针来表示一个单链表,头指针为Null时,表示这是一个空链表
在单链表的第一个节点之前附加一个结点,成为头结点。头结点的数据域不设信息,指针域指向线性表的第一个元素的结点。
头结点和头指针的区分:头结点有无,头指针始终指向链表的第一个结点,而头结点是带头结点链表中的第一个结点,结点内通常不存储信息。
优点:避免像顺序表一样需要大量的连续的存储空间,
缺点:但是单链表附加指针域,也存在浪费存储空间的现象
引入头结点的优点:由于开始结点位置被在存放头结点的指针域中,所以在链表的一个位置上的操作和其他位置上的操作一致,无需进行特殊处理;无论链表是否为空,其头指针是指向头结点的非空指针(空表中头结点的指针域为空)。因此,空表和非空表的处理就完全一致了。
2.1.1 单链表的基本操作实现
(1)采用头插法建立单链表L
//C语言实现的关键语句
//s为插入的结点,L为生成的链表
s->data=x;
s->next=L->next;
L->next=s;
采用头插法建立链表,读入数据的顺序和生成的链表中元素的顺序是相反的,每个结点插入时间为O(1),设单链表的长度为n,z则总的时间复杂度为O(n)。
(2)采用尾插法建立单链表
必须增加一个尾指针,使其指向当前链表的尾结点。时间复杂度和头插法相同。
//s为插入的结点,r 为尾指针
s->data=x;
r->next=s;
r=s;
(3)按序号查找结点值中从第一节点出发,顺指针next域逐个往下搜索,直到找到第 i个结点为止,否则返回最后一个结点指针域NULL。
//C语言代码实现
LNode *GetElem(LinkList L, int i){
LNode *p=L->next;
if(i==0)
return L;
if (i<1)
return NULL;
while(p&&jnext;
j++;
}
return p;
}
按序号查找操作的时间复杂度为O(n)
(4)按值查找表结点
从单链表第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的等于给定值e,则返回给定值e,则返回该结点的指针;若找不到这样的结点,则返回NULL。
//C语言实现
LNode *LocateELem(LinkList L,ElemType e){
Lnode *p=L->next; //从第一个结点
while (p!=NULL&&p->!=e)
p=p->next;
return p;
}
按值查找操作的时间复杂度为O(n)
(5)插入结点操作
插入操作 是将值为x的新结点插入到单链表的第i个 位置上。先检查 插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,在其后插入新的结点。
//按序号查找,找到i-1结点,然后插入新结点
p=GetElem(L,i-1);
s->next=p->next;
p->next=s;
以上为后插操作,前插操作与后插操作恰恰相反,可以将其转换成后插操作,前提是从单链表的头结点开始顺序查找到其前驱结点,时间复杂度为O(n),只进行数据域的交换就可以啦
//将*s 结点插入*p 之前的主要的代码片段
s->next=p->next;
p->next=s;
temp=p->data;
p->data=s->data;
s->data=temp;
(6)删除操作
删除操作是将单链表的第i个结点。先检查删除位置的合理性,然后查找表中第i-1个结点,即被删除结点的前驱结点,再将其删除。
//*p 为前驱结点,*q 为要删除的结点
p=GetElem(L,i-1);
q=p->next;
p->next=q->next;
free(q);
和插入算法一样,该算法的主要事件也是耗费在查找操作上,时间复杂度为O(n)。
扩展删除结点*p,可以通过删除*p的后续结点操作来实现,实质就是将其后续结点的值赋予其自身,然后删除后继结点,时间复杂度O(n)。
q=p->next;
p->data=p->next->data;
p->next=q->next;
free(q);
(7)求表长操作(不含有头结点)
算法实现的时间复杂度为O(n)
2.1.2 双链表
双链表仅在单链表结点中增长了一个指向一个其前驱的prior 指针,因此,在双链表中执行按值查找和按位查找的操作和单链表相同,但是在插入和删除操作的实现上,在单链表中有着较大的不同。需要对prior指针做出修改。
双链表很容易找到其前驱结点,因此,插入删除结点的算法的时间复杂度仅为O(1)。
(1)双链表的插入操作
在双链表中 p所指的结点之后插入结点*s。
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
(2)双链表的删除操作
删除双链表中结点*p的后继结点*q;
p->next=q->next;
pq->next->prior=p;
free(q);
2.1.3循环链表
2.1.3.1 循环单链表
与单链表的区别在于表中最后一个结点的指针不是NULL,而改为指向头结点,从而整个链表形成一个环。
判断空链表的条件为:不是头结点是否为空,而是尾指针的next域是否等于头指针。
插入和删除操作无需判断是否为表尾
2.1.3.1 循环双链表
头结点的prior指针还要指向表尾结点
在循环双链表L中,某结点*p为结点时,p->next==L;当循环双链表我空表时,起头结点的prior域和next域都等于L。
2.1.4静态链表
静态链表是借助线性表的链式存储结构,结点也有数据域data和指针域next。但这里指的是结点的相对地址,和顺序表一样,静态链表也要预先分配一块连续的内存空间。
2.1.5 顺序表和链表的比较
(1)存储方式
顺序表可以顺序存储,也可以随机存取,链表只能从表头顺序存取元素。
(2)找、插入和删除操作
对于按值查找,当顺序表在无序的情况下,两者的时间复杂度均为O(n);而当顺序有序时,可采用折半查找,此时时间复杂度为O(log2n)