关于数据结构,可以大致做如下分类
当需要存储一系列元素时,我们可能会想到数组,但是数组增删元素,又要改变其他元素的索引,十分麻烦,这时我们就需要链表。
目录
目录
一、链表是什么
链表是由节点和指针组成的数据结构,每个节点存有一定的值,和一个指向下一个节点的指针,因此很多链表问题可以用递归来处理。
链表就像寻宝游戏,需要读取链表的最后一个元素时,不能直接读取,因为不知道它所处的地址,必须先访问元素1,从而获取元素2的地址,再访问元素2并从中获取元素3的地址。以此类推,直到访问最后一个元素。
链表与数组:
由于链表中的元素存储在内存的各个地方,所以链表擅长插入和删除,
同时链表必须读取所有元素,当需要随机读取元素时,应选择数组。.
链表和数组关系十分密切,但在平常中,往往是数组用的更多。
二、链表的组成与结构
结构:
head为 头指针 ,
最后一定会指向空集,空集的指针表示为-1,即 ne[n]=-1。
表示方法:
struct node
{
int val;
struct node * next;
};
三、链表操作
1.求表长
采用链表遍历的方法来求
先设一个临时的指针p,指向链表的头(ptrl),把计数器 j 一开始设为0,然后开始遍历,只要p没有指向null,循环就不会结束
int length (list ptrl)
{
list p=ptrl; //p指向表的第一个节点
int j=0;
while(p){
p = p->next;
j++; //当前p指向的是第j个节点
}
return j;
}
2.查找
仍然采用遍历的方法
先把临时的指针变量p设为表头(ptrl),用 i 表示第几个元素,接下来只要p没有指向null,并且还没有找到第k个元素,循环就不会结束,直到找到第k个元素
list FindKth( int k,list ptrl)
(
list p = ptrl;
int i=1;
while(p!=null && i<k){
p=p->next;
i++;
}
if(i==k) return p; //找到第k个,返回指针
else return null;
}
3.插入
在第 i-1个节点后插入值为x的新节点
链表的插入,一定要知道插的节点的前面一个是是谁
具体步骤:(动态插入)
先用malloc这个函数申请一块空间,构造一个新节点,用s指向
再找到链表的第 i-1个节点,用p指向
修改指针,插入节点(也就是说,在p后插入s)
list Insert( ElementType x, int i, list ptrl)
{
list p,s;
if(i==1){
s = (list)malloc (sizeof(struct LNode)); //让新节点插在表头
s->data =x;
s->next =ptrl;
return s; //返回新表头指针
}
p= FindKth( i-1,ptrl); //查找第i-1个节点
if(p == null){
cin>>"参数i出错";
return null;
}
else{
s = (list)malloc (sizeof(struct LNode)); //申请,换装节点
s->data =x;
s->next =p->next; //新节点插在第i-1个节点后面
p->next =s;
return ptrl;
}
}
4.删除
删除链表的第 i 个节点
(要把第 i 个节点删除,前面一个节点就要接到后面去)
具体步骤:
先找到链表的第 i-1个节点,用p指向
再用指针s指向要被删除的节点(p的下一个节点)
然后修改指针,删除s所指节点
最后通过free函数释放所指节点的空间
list Delete(int i, list ptrl)
{
list p,s;
if(i==1){ //如果要删除第一个节点
s=ptrl; //s指向第一个节点
if(ptrl != null) ptrl=ptrl->next; //从链表中删除
else return null;
free(s);
return ptrl;
}
p=FindKth( i-1,ptrl); //查找第i-1个节点
if(p==null){
cout<<"第i-1个节点不存在“;
return null;
}
else if(p->next==null){
cout<<"第i个节点不存在";
}
else{
s=p->next; //s指向第i个节点
p->next = s->next; //从链表中删除
free(s); //释放被删除节点
return ptrl;
}
}
进行链表操作时的小技巧
在进行链表操作时,尤其是删除节点时,经常会因为对当前节点进行操作,而导致内存或指针出现问题
有两个小技巧可以解决这个问题:
一是尽量处理当前节点的下一个节点,而非当前节点本身;二是建立一个虚拟节点(dummy node),使其指向当前链表的头节点,这样,即使原链表所有节点全部删除,也会有一个dummy存在,返回dummy->即可。
四、单链表
其中上述e[ ],ne[ ]这种用数组模拟链表的叫做 静态链表,这种速度较快,比较常用;
最大的用途是可以来写邻接表,而邻接表最常用的是存储图和树。
例1:向链表头插入一个数(把红色的点变成第一个)
#include<iostream>
using namespace std;
const int n=101100;
//head表示头节点的下标
//e[i]表示节点i的值
//ne[i]表示节点i的next指针是多少
//idx 存储当前已经用到了哪个点
int head, e[n], ne[n], idx;
//初始化
void init()
{
head=-1;
idx=0;
}
//将x插到头节点 //常用!!!!
void add_to_head(int x)
{
e[idx]=x;
ne[idx]=head; //①操作
head=idx; //②操作
idx++;
}
例2:删除第 k 个节点后面的数
#include<iostream>
using namespace std;
const int n=101100;
//head表示头节点的下标
//e[i]表示节点i的值
//ne[i]表示节点i的next指针是多少
//idx 存储当前已经用到了哪个点
int head, e[n], ne[n], idx;
//初始化(任何时候都别忘了)
void init()
{
head=-1;
idx=0;
}
void remove(int k)
{
ne[k] = ne[ne[k]]; //①操作
}
五、双链表
每个节点有两个指针,一个指向前,另一个指向后。通常用 l[n]表示左边指向的谁,r[n]表示右边指向的谁。
在本文规定下标为0表示head,下标为1表示最后一个点。
双链表常用来优化某些问题。
例1:插入某个点
#include<iostream>
using namespace std;
const int n=101100;
//e[i]表示节点i的值是多少
//idx 存储当前已经用到了哪个点
int e[n], l[n], r[n], idx;
//初始化
void init()
{
//0表示左端点,1表示右端点
r[0]=1, l[1]=0;
idx=2;
}
//在下标为k的点的右边,插入x
void add(int k, int x)
{
e[idx]=x; //先赋值
r[idx]=r[k]; //①操作
l[idx]=k; //②操作
l[r[k]]=idx; //③操作
r[k]=idx; //④操作
}
//在下标为k的点的左边,插入x,调用上面的add函数
add(l[k],x);
例2:删除第k个点
#include<iostream>
using namespace std;
const int n=101100;
//e[i]表示节点i的值是多少
//idx 存储当前已经用到了哪个点
int e[n], l[n], r[n], idx;
//初始化
void init()
{
//0表示左端点,1表示右端点
r[0]=1, l[1]=0;
idx=2;
}
//删掉第k个点
void remove(int k)
{
r[l[k]]=r[k]; //①操作
l[r[k]]=l[k]; //②操作
}
六、模板
单链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 在链表头插入一个数a
void insert(int a)
{
e[idx] = a, ne[idx] = head, head = idx ++ ;
}
// 将头结点删除,需要保证头结点存在
void remove()
{
head = ne[head];
}
双链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 在链表头插入一个数a
void insert(int a)
{
e[idx] = a, ne[idx] = head, head = idx ++ ;
}
// 将头结点删除,需要保证头结点存在
void remove()
{
head = ne[head];
}