分类
Level5

链表

一、链表的概念
线性表的优点
A、无需为表示结点间的逻辑关系而增加额外的存储空间;
B、可方便地随机存取表中的任一元素。
线性表的缺点
A、插入或删除平均需要移动一半的结点;
B、顺序表要求占用连续的存储空间。
注意链表结构并不一定占用连续的内存空间,其占用的内存空间可以不连续!
链表的基本概念
A. 结点(Node)组成:数据域,指针域
B. 数据域:存储数据元素本身
C. 指针域:存储邻接元素的存储地址(位置)
D. 链接方式:单链表:每个结点只有一个指针域。双向链表:每个结点有两个指针域。循环链表:是一个首尾相接的链表。
E. 头指针:指向链表头结点的指针。
链表的存储方式:链表可以通过指针来链接节点,因此不需要把数据存储在连续的空间上,所以删除和增加数据都很方便。
二、单向链表
◆ 指针是单向的,只能从左向右单向遍历数据。
◆ 循环链表:首尾相接,尾巴tail的next指针指向头部head的data。
◆ 有时场景比较简单,不需要循环,可以让第一个节点始终是头。
三、双向链表
有两个指针,pre指针指向前一节点,next指针指向后一节点。
循环:最后节点的next指针指向第一个节点,第一个节点的pre指针指向最后的节点。
四、链表的缺点
和数组相比,链表的缺点是查找慢
例如查找data等于某个值,需要遍历整个链表才能找到,计算量O(n);而数组中通过下标可以直接访问到任意一个元素。
五、用数组模拟链表(静态链表):“逻辑上是链表,物理上是数组”。

const int N = 10000;  //按需要定义静态链表的空间大小
struct node{          //单向链表
    int id;           //这个节点的id
    int data;         //数据
    int nextid;       //指向下一个节点的id
}nodes[N];            //静态分配需要定义在全局

//为单链表的next指针赋初值,例如:
    nodes[0].nextid=1;
    for(int i=1;i<=n;i++){
        nodes[i].id=i; //把第i个节点的id就赋值为i
        nodes[i].nextid=i+1;//next指针指向下一个节点
    }

    nodes[n].nextid=1;//循环链表:尾指向头即可         
//遍历链表:沿着nextid访问节点即可
//删除节点:比如删除now节点,找到now的前一个节点
    nodes[preid].nextid = nodes[now].nextid;//跳过节点now,即删除now
    now = nodes[preid].nextid; //更新now
//插入节点,略

六、用指针实现链表

//单链表:头插法
#include <iostream> 
using namespace std;
int n,x;
struct Node{  //结构体
	int val;      //值
	Node *next;   //下一个结点
}*head;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){ //生成n个结点的链表
        cin>>x;
    	Node *p=new Node();  //定义一个新的结点
    	p->val=x;
    	p->next=head;
    	head=p;
    }
    for(Node *p=head;p;p=p->next){//从头开始输出
	cout<<p->val<<' ';
    }
    cout<<endl;
    return 0;
}

单链表的插入和删除
1、插入
(1)在链表中间的p节点后面插入节点s【注意顺序不能交换)
☆ s->next=p->next
☆ p->next=s
(2)在链表的表头 head 后面插入节点 s【注意顺序不能交换)
☆ s->next=head
☆ head=s
2、删除:删除p节点后面的节点
☆ p->next=p->next->next

//双链表
#include <iostream> 
using namespace std;
int n,x;
struct Node{  //结构体
	int val;  //值
	Node *next,*pre;//next下一个结点,pre下一个结点
}*head,*tail;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){ //生成n个结点的链表
        cin>>x;
    	Node *p=new Node();  //定义一个新的结点
    	p->val=x;
    	p->next=head;
        if(p->next) p->next->pre=p; 
    	head=p;
        if(i==1) tail=p;
    }
    for(Node *p=head;p;p=p->next){//从头开始输出	 
	cout<<p->val<<' ';
    }
    cout<<endl;
    for(Node *p=tail;p;p=p->pre){//从尾开始输出	 
	cout<<p->val<<' ';
    }
    cout<<endl;
    return 0;
}

双链表的插入和删除
1、插入
(1)在p节点后面插入节点s
☆ s->pre=p
☆ s->next=p->next
☆ p->next->pre=s
☆ p->next=s【必须放在最后】
(2)在p节点前面插入节点s
☆ s->pre=p->pre
☆ s->next=p
☆ p->pre->next=s
☆ p->pre=s【必须放在最后】
2、删除
(1)删除p节点后面的节点
☆ p->next->next->pre=p
☆ p->next=p->next->next
(2)删除p节点前面的节点
☆ p->pre->pre->next=p
☆ p->pre=p->pre->pre
(3)删除p节点
☆ p->pre=p->next
☆ p->next=p->pre

//循环链表
#include <iostream> 
using namespace std;
int n,x;
struct Node{  //结构体
	int val;  //值
	Node *next,*pre;//next下一个结点,pre下一个结点
}*head,*tail;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){ //生成n个结点的链表
        cin>>x;
    	Node *p=new Node();  //定义一个新的结点
    	p->val=x;
    	p->next=head;
        if(p->next) p->next->pre=p; 
    	head=p;
        if(i==1) tail=p;
    }
    tail->next=head;
    head->pre=tail;
    int m=2*n;//准备输出2圈
    for(Node *p=head;m;p=p->next,m--){//从头开始输出	 
	cout<<p->val<<' ';
    }
    cout<<endl;
    int k=3*n;//准备输出3圈
    for(Node *p=tail;k;p=p->pre,k--){//从尾开始输出	 
	cout<<p->val<<' ';
    }
    cout<<endl;
    return 0;
}

七、在编写程序时,链表还可以直接使用C++的STL list

操作说明
size()链表元素个数
empty()链表是否为空
push_front()在链表头添加元素
push_back()在链表末尾添加元素
unique()将链表中相邻的重复元素去除,如果重复元素不相邻,则无效
sort()升序排序
pop_back()删除链表尾
pop_front()删除链表头
front()读队头
back()读队尾
reverse()反转链表
insert(it)在it位置插入元素

在信息学竞赛中,我们经常使用数组模拟链表,比如邻接表