链表是一种用于存储数据的数据结构,通过如链条一般的指针来连接元素。它的特点是插入与删除数据十分方便,但查找与读取数据的表现欠佳。

1、单链表(只有一个指向后继元素的指针)

2、双链表(既有指向后继元素的指针,也有指向前驱元素的指针)

3、循环链表(最后一个元素的指针指向第一个元素,形成一个环)

为什么需要链表

数组就是一块连续的内存空间,有了这块内存空间的首地址,就能直接通过下标计算出任意位置的元素地址。

链表不一样,链表并不需要一整块连续的内存空间存储元素。链表的元素可以分散在内存空间的天涯海角,通过每个节点上的 next, pre 指针,将零散的内存块串联起来形成一个链式结构。

这样做的好处很明显,首先就是可以提高内存的利用效率,链表的节点不需要挨在一起,给点内存 new 出来一个节点就能用,操作系统会觉得这娃好养活。

另外一个好处,它的节点要用的时候就能接上,不用的时候拆掉就行了,从来不需要考虑扩缩容和数据搬移的问题,理论上讲,链表是没有容量限制的(除非把所有内存都占满,这不太可能)。

当然,不可能只有好处没有局限性。数组最大的优势是支持通过索引快速访问元素,而链表就不支持。

单链表

链表由一系列的结点组成(链表中的每个元素都可称为结点),对于单链表而言,它的结点包含两部分: 数据域:存储当前结点的数据 指针域:存储当前结点的下一个结点(后继结点)的地址

struct node{
    int data; //存数据
    node *next;//存下一个数据点的地址
};

在使用之前,首先要定义一个空链表(NULL),并用一个指针head(通常称为头指针)指向该链表;

node *p,*head=NULL;//定义节点p和链表头结点,NULL是常量,代表空
p=new node();//向系统申请内存空间
p->data=10;//指针的成员变量访问符为->
//(*p).data=x;//指针的另外一种写法 
delete p;//使用完结点,需要释放该结点占用的空间

使用单链表存储 n 个数据

#include<bits/stdc++.h>
using namespace std;
struct node{
    int data; //存数据
    node *next;//存下一个数据点的地址
};
//输入n个数据,存储在一个单链表中
node *p,*head=NULL; 
void create(){
    int n,x;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x;
        p=new node();
        p->data=x;
        p->next=head;
        head=p;
    }
} 
void print(){
    node *q=head;
    while(q!=NULL){
        cout<<q->data<<" ";
        q=q->next;
    }
}
int main() { 
    create();
    print();
    return 0;
}

每次新输入的数据都插入在头结点,简称头插法。在CSP中,一般不会考察如何构建一个单链表,重点考察如何在链表中插入和删除元素,这是每年的重要考点。

插入

如果已知单链表中 P 和 Q 是前后相邻的两个结点,现在需要在 P 和 Q 结点之间,插入新结点 R ,需要完成的操作是什么?

可以将问题分为几个步骤:

1、首先明确P和Q的关系,如图所示:P->next为Q。

2、要在P和Q结点之间插入新结点R,则需要完成两步操作:先把R的next指针指向Q,然后将P的next指针指向R。

R->next=P->next;//将新结点R的next指针指向Q结点,即原来p->next就是Q
//R->next=Q;//Q本身就是指针变量,这样也可以
P->next=R;//再将P结点的next指针指向R结点

删除

单链表结点P、Q、R是相邻的三个结点,要求删除Q结点。

分为两步:

1、将P结点的next指针指向R结点

2、释放Q结点的内存(delete操作)

p->next=Q->next;
delete Q;

双链表

单链表的数据之间的连接是单向的,而双链表数据元素之间的连接是双向的,可以前后互相寻址,因此双链表相邻数据之间的逻辑关系是如下图所示。

双链表的每一个结点,包含三个部分:

1、数据项,存储这个结点的数据。

2、前驱指针,这个指针指向前一个结点的地址。

3、后去指针,这个指针指向下一个结点的地址。

struct dnode{
    int data;//数据项
    dnode *prev;//前驱指针
    dnode *next;//后继指针
};

手写双链表很少考核,重点还是双链表的结点插入和删除操作。

插入

双链表的结点插入:在相邻的两个结点P和Q之间插入结点R,需要完成的操作步骤。

由于双链表每个结点都有前驱指针和后继指针,因此操作比单链表复杂一些,相邻两结点之间插入新结点前后如何所示:

插入结点后(虚线为新的指针指向示意)

在双向链表中插入结点需要修改三个指针:前驱结点的后继指针,新结点的前驱指针、新结点的后继指针。具体步骤:

1、找到插入位置的前驱结点P和后继结点Q

2、创建新结点R,将R的前驱指针指向P,后继指针指向Q

3、修改P的后继指针为R,Q的前驱指针为R

R->next=P->next;//将原来P的next指针赋值给R的next,相当于把R->next指向Q
R->prev=Q->prev;//将原来Q的prev指针赋值给R的prev,相当于把R->prev指向P
P->next=R;//把P的next指针指向新结点R
Q.prev=R;//把Q的prev指针指向新结点R

删除

双链表的结点删除:在相邻的三个结点P、Q、R,删除Q结点需要完成的操作步骤。

双链表的结点删除操作比插入操作要简单一些,在双向链表中删除结点需要修改三个指针:前驱结点的后继指针、后继结点的前驱指针、待删除结点。具体步骤:

1、找到待删除结点Q的前驱结点P和后继结点R

2、修改前驱结点P的后继指针为Q的后继结点

3、修改后继结点R的前驱指针为Q前驱结点P

4、删除结点Q,释放内存空间

P->next=Q->next;//修改前驱结点P的后继指针为Q的后继结点
R->prev=Q->prev;//修改后继结点R的前驱指针为Q的前驱结点P
delete Q;//释放空间

循环链表

1、单向循环链表

2、双向循环链表

单向循环链表的数据结点插入、删除操作与单链表一致。

双向循环链表的数据结点插入、删除操作与双链表一致。

​ 单向循环链表 ​ ​ 双向循环链表

STL 中的list

STL中的list是一种双向链表,可高效进行插入和删除元素。需要在机试题中使用链表,一般可以使用list。

list<node> lst;//node指的是数据元素的类型,lst是双链表的名称

list中还有多个函数课供调用:

函数名 功能 复杂度
size() 返回元素数 O(1)
begin() 返回指向表开头的迭代器 O(1)
end() 返回指向表末尾的迭代器 O(1)
push_front(x) 在表开头添加元素x O(1)
push_back(x) 在表的末尾添加元素x O(1)
pop_front() 删除表头元素 O(1)
pop_back() 删除表尾元素 O(1)
insert(p,x) 在位置p插入元素x O(1)
erase(p,x) 删除位置p的元素 O(1)
clear() 删除表中所有的元素 O(1)
#include<bits/stdc++.h>
using namespace std;
list<int> lst;
list<int>::iterator it;//定义迭代器 it,含义与指针类似
int n,x; 
int main() { 
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x;
        lst.push_back(x);
    }
    for(it=lst.begin();it!=lst.end();it++){
        //需要注意的是循环条件it!=lst.end(),不能用大于小于来判断,必须用不等于
        cout<<*it<<" ";  //it是指针 
    }
    return 0;
}

历年初赛真题回顾

1、(CSP-J2019) 链表不具有的特点是()

​ A. 插入删除不需要移动元素

​ B.不必事先估计存储空间

​ C.所需空间与线性表长度成正比

​ D.可随机访问任一元素

2、(CSP-J2020)链表不具有的特点是()。

​ A.可随机访问任一元素

​ B.不必事先估计存储空间

​ C.插入删除不需要移动元素

​ D.所需空间与线性表长度成正比

3、(CSP-J2022)运行以下代码片段的行为是( )。

int x = 101;
int y = 201;
int *p = &x;
int *q = &y;
p = q;

​ A.将 xx 的值赋为 201201

​ B.将 yy 的值赋为 101101

​ C.将 qq 指向 xx 的地址

​ D.将 pp 指向 yy 的地址

4、链表和数组的区别包括( )。

A.数组不能排序,链表可以

B.链表比数组能存储更多的信息

​ C.数组大小固定,链表大小可动态调整

​ D.以上均正确

5、以下哪组操作能完成在双向循环链表结点 pp 之后插入结点 ss 的效果(其中,next 域为结点的直接后继,prev 域为结点的直接前驱):( )。

A. p->next->prev=s; s->prev=p; p->next=s; s->next=p->next;
B. p->next->prev=s; p->next=s; s->prev=p; s->next=p->next;
C. s->prev=p; s->next=p->next; p->next=s; p->next->prev=s;
D. s->next=p->next; p->next->prev=s; s->prev=p; p->next=s;

6、假设有一个链表的节点定义如下:

struct Node { int data; Node* next; }

现在有一个指向链表头部的指针:Node* head。如果想要在链表中插入一个新节点,其成员 data 的值为 42,并使新节点成为链表的第一个节点,下面哪个操作是正确的?

A. Node* newNode = new Node; newNode->data = 42; newNode->next = head; head = newNode;
B. Node* newNode = new Node; head->data = 42; newNode->next = head; head = newNode;
C. Node* newNode = new Node; newNode->data = 42; head->next = newNode;
D. Node* newNode = new Node; newNode->data = 42; newNode->next =head;
Copyright ©图灵之星 2024,转载需注明出处该文件修订时间: 2025-03-25 20:00:16

results matching ""

    No results matching ""