1.写在前面
众所周知,链表的最常见的数据结构之一。此外,提起链表,不得不说“数组”,这两者经常用来比较。学习链表和数组最重要
明确两者的内存布局以及在什么场合使用。下面对比下两者的区别
2.数组VS链表
2.1 数组
内存布局:定长的连续内存空间
优劣势:
优势:.“下标查找”的时间复杂度为O(1)(注意是“下标查找”),即具有“随机访问”的特点
劣势:1)增加和删除的时间复杂度为O(n),即需要移动大量的元素以腾出空间(增加)或移动大量元素以覆盖(删除)
2)不能动态拓展,数组空间需提前确定,需要扩容时,需重新分配
注意:数组越界
普及两点:
1)数组下标为何从0开始?
如果下标从1开始:a[k].address = base_address + (k-1)* type_size
而下标从0开始:a[k].address = base_address + k * type_size
相比之下,下标从0开始省去了一个减法运算,而数组效率的优化要尽可能极致,故减少一次减法操作
2)有容器还需数组吗?
如C++的vector,java的ArrayList容器类将很多操作的细节封装起来,且支持动态扩容,但最后再创建这些容器类时事先
指定合适数据大小,因为扩容操作设计内存申请和数据搬移,是比较耗时的尽管容器类给开发带来很多方便,但数组的
用武之地仍是有的,如如果是底层开发,性能需要优化到极致,使用数组而不是容器类。而容器类适合业务开发
2.2 链表
内存布局:不需要连续,只需存储下一个节点的地址即可
优劣势:
优势:1)增删的时间复杂度为O(1), 速度较数组快
2) 扩容方便
劣势:查找的时间复杂度为O(n),即查找代价高,不具备随机访问,需从头到尾遍历
2.3 "应用场合" 及 "空间利用率"
应用场合:两者可以说是优势互补。在实际应用中,如果要求大量的查找,那数组更合适。反之,在大量增删操作的场合,
链表更胜一筹。
那如果是要求大量增删且查找呢? 可用“数组链表”,即分配的空间是连续,但以链表方式操作。(如内核的Hash链表)
内存空间利用率:每一个数组单元是 100% 存储数据,每一个链表单元是存储数据 + 存储指针,数组对于内存的利用上大于链表
(当然了,还需要看是否提前知道要存储数据个数)
2.4 总结一下
1.内存布局:数组分配的是连续内存,不易引起空间碎片化,但空间利用率相对链表低,且大小固定。而链表可引起空间碎片化,
但空间使用灵活,大小可动态分配,空间利用率高,但随动态分配和释放也会降低执行效率
2.性能:数组查询快,但删除、插入慢。反之,链表插入、删除快,查询慢
(注意:比较是相对的)
3.复杂度:链表插入删除时间复杂度O(1),随机访问O(n),数组与之相反
3.链表的模型
链表分为 4 种情况:单链表,单循环链表,双链表,双循环链表
各自模型如下:(截图自谱哥的微信公众号“编程剑谱”,强烈推荐关注。“认真的人自带光芒”)
4. 链表的操作
说到链表的操作,CRUD(增删改查)必不可少,也是耳熟能详的话题了。
4.1 单链表
根据有无头结点,可分为两种情况
1)带头结点(不存储真实数据)的单链表,如下图示
2)不带头结点的单链表
3)下面实现一个有序单链表
头文件
#ifndef _LLIST_H_
#define _LLIST_H_
#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;
typedef struct SingleNode
{
ElemType data;
struct SingleNode *next;
}SingleLinkedList,*LinkList;
//单链表初始化
void ListInitialize(LinkList *head)
{
if((*head=(SingleLinkedList*)malloc(sizeof(SingleLinkedList)))==NULL)
exit(1);
(*head)->next=NULL;
}
//插入
void ListInsert(LinkList head,ElemType x)
{
LinkList curr,pre,q;
curr=head->next;
pre=head;
while(curr!=NULL&&curr->data<=x)
{
pre=curr;
curr=curr->next;
}
q=(SingleLinkedList *)malloc(sizeof(SingleLinkedList));
q->data=x;
q->next=pre->next;
pre->next=q;
}
//删除数据元素
int ListDelete(LinkList head,ElemType x)
{
LinkList p,s;
s=head;
p=head->next;
while(p->next->next!=NULL&&p->next!=NULL&&x!=p->data)
{
s=s->next;
p=p->next;
}
if(p->next==NULL)
return 0;
s->next=p->next;
free(p);
return 1;
}
//销毁
void ListDestroy(LinkList *head)
{
LinkList p,p1;
p=*head;
while(p!=NULL)
{
p1=p;
p=p->next;
free(p1);
}
*head=NULL;
}
#endif
.c文件
#include"Llist.h"
int main(void)
{
SingleLinkedList *head,*p,*q;
int n,i;
ListInitialize(&head);
p=head;
printf("head:%p", p->next);
//添加有序单链表的元素 ,并打印出来
puts("*********有序单链表***********");
puts("有序单链表的数据元素显示:");
for(i=0;i<15;i++)
{
printf("%d->", i+1);
q=(SingleLinkedList *)malloc(sizeof(SingleLinkedList));
q->data=i+1;
q->next=p->next;
p->next=q;
p=q;
}
puts("");
puts("");
//插入操作
ElemType d;
puts("*********插入操作***********");
printf("请输入需要插入的数据:");
scanf("%d",&d);
ListInsert(head,d);
puts("");
//删除操作
puts("*********删除操作***********");
printf("请输入需要删除的数据:");
scanf("%d",&d);
if(!(ListDelete(head,d)))
puts("删除失败!");
puts("");
puts("**********打印*************");
p=head;
puts("修改后的有序单链表:");
for(i=0;i<16;i++)
{
if(p->next == NULL)
break;
p=p->next;
printf("%d ",p->data);
}
puts("");
puts("*****************************");
retun 0;
运行结果
4.2 双向循环链表
4.2.1 双向链表:空间换时间的设计思想
相比单链表
删除/插入操作,有两种情况:1)给定值 2)给定指向结点指针
针对1)情况,都需要从头结点遍历,时间复杂度O(n)
针对2)情况,单链表要从头结点找到要删除结点的前驱结点,时间复杂度O(n),而双链表不需要,时间复杂度O(1)
查询:双链表可记录上次查找的位置p,每次查询可与p比较大小,从而确定向前查询,还是向后。
故查询效率比单链表高些
4.2.2 双向循环链表的模型图
以下“双向循环链表”的代码实现来自
https://mp.weixin.qq.com/s/QIQM7MxAGb5sovrtSqRC2A
用了“面向对象”和“面向工具”的思维编写。函数命名上遵循STL的标准函数命名,赞!
1)C++代码
1#ifndef _DCLINK_H_
2#define _DCLINK_H_
3
4#include<iostream>
5#include<stdlib.h>
6using namespace std;
7
8template<typename Type>
9class DCLink;
10
11template<typename Type>
12class DCLinkNode{ //这是双向循环链表的结点类型
13 friend class DCLink<Type>;
14public:
15 DCLinkNode(Type x = 0){
16 prev = next = NULL;
17 data = x;
18 }
19 ~DCLinkNode(){
20
21 }
22private:
23 Type data; //数据域
24 DCLinkNode *prev; //前驱结点指针
25 DCLinkNode *next; //下一个结点指针
26};
27
28template<typename Type>
29class DCLink{
30public:
31 DCLink(){ //初始化对象的构造函数
32 DCLinkNode<Type> *s= new DCLinkNode<Type>;
33 first = last = s;
34 s->next = first;
35 s->prev = last;
36 size = 0;
37 }
38 ~DCLink(){
39
40 }
41public:
42 bool push_back(const Type &); //尾随函数
43 void show_link()const; //显示链表
44 bool push_front(const Type &); //前插函数
45 bool insert_val(); //根据值得插入
46 bool pop_back(); //删除最后一个结点
47private:
48 bool insert_pos();
49private:
50 DCLinkNode<Type> *first; //永远指向链表第一个结点
51 DCLinkNode<Type> *last; //永远指向链表的最后一个结点
52 size_t size; //统计结点个数,不算没有数据的第一个;
53};
54
55template<typename Type>
56bool DCLink<Type>::push_back(const Type &x){
57 DCLinkNode<Type> *s = new DCLinkNode<Type>(x);
58 if(s == NULL){
59 return false;
60 }
61 s->next = first;
62 s->prev = last;
63 last->next = s;
64 first->prev = s;
65 last = s;
66 size++;
67
68 return true;
69}
70
71template<typename Type>
72void DCLink<Type>::show_link()const{
73 DCLinkNode<Type> *p = first->next;
74
75 while(p != first){
76 cout<<p->data<<"--> ";
77 p = p->next;
78 }
79 cout<<"NULL"<<endl;
80}
81
82template<typename Type>
83bool DCLink<Type>::push_front(const Type &x){
84 DCLinkNode<Type> *s = new DCLinkNode<Type>(x);
85 if(s == NULL){
86 return false;
87 }
88 s->next = first->next;
89 first->next->prev = s;
90 s->prev = first;
91 first->next = s;
92 size++;
93
94 return true;
95}
96
97template<typename Type>
98bool DCLink<Type>::insert_val(){
99 int number1;
100 int number2;
101
102 cout<<"请输入要插入位置的值 :";
103 cin>>number1;
104 cout<<"请输入要插入的数据";
105 cin>>number2;
106
107 DCLinkNode<Type> *p = first->next;
108 DCLinkNode<Type> *s = new DCLinkNode<Type>(number2);
109 while(p != first){
110 if(p->data == number1){
111 s->prev = p->prev;
112 s->next = p;
113 p->prev->next = s;
114 p->prev = s;
115 size++;
116 }
117 p = p->next;
118 }
119
120 return true;
121}
122
123template<typename Type>
124bool DCLink<Type>::pop_back(){
125 DCLinkNode<Type> *tmp = last->prev;
126 DCLinkNode<Type> *tmp1 = last;
127
128 tmp->next = first;
129 first->prev = tmp;
130 last = tmp;
131 size--;
132
133 delete tmp1;
134 tmp1 = NULL;
135
136 return true;
137}
138
139#endif
2)测试代码
1#include<iostream>
2#include"dclink.h"
3using namespace std;
4
5int main(void){
6 DCLink<int> dc;
7
8 for(int i = 0; i < 10; i++){
9 dc.push_back(i);
10 }
11
12 dc.push_front(-1);
13 dc.show_link();
14 dc.insert_val(); //均是前插
15 dc.show_link();
16 dc.pop_back();
17 cout<<"删除最后一个结点时如下: "<<endl;
18 dc.show_link();
19
20 return 0;
21}
22#endif
3)测试结果
参考资料:
1)https://mp.weixin.qq.com/s/QIQM7MxAGb5sovrtSqRC2A(推荐关注“编程剑谱”微信公众号)
2)极客时间的《数据结构与算法之美》专栏