链表分为单链表、双向链表和循环链表,这三种数据结构本质上就是指针的灵活使用,只要理解了单链表的一些操作,那么剩下两种也能快速掌握。单链表的每一个节点不同于数组是连续的,每一个节点都考前一个节点确定后才能确定。
单链表通常由一个头指针指向单链表的第一个节点,每个节点通常是一个结构体,结构体内部包含一个指针,该指针指向下一个地址。在初始化创建单链表时有两种方式,一种是不包含头结点的(上方),一种是包含头结点的(下方)。
头结点与普通的节点使用同一种结构体但只是用指针,通常采用带头节点的方式,这样在插入删除时,不需要使用分支结构来判断是否要用到头结点。
单链表初始化
以最基本的结构体为例,内部只包含整形元素和指针,将Head传入初始化函数中以引用的形式接收,让头指针连接头结点并初始化指针,完成单链表的初始化。最终形成的逻辑结构与上图中第二个结构一样,不包含节点一。
typedef struct List {
int data;
struct List* next;
}List;
void Init_List(List* &Head)
{
Head = new List();
Head->next = NULL;
}
int main()
{
List* Head = NULL;
Init_List(Head);
}
单链表插入节点
如果不需要注意数据的顺序,可以采用头插法,直接放入头指针后面即可,这样效率最高。如果需要按照一定顺序或者插入某一个位置,需要一个遍历指针来找插入位置的前驱。如下图所示,假设需要在节点1与节点2之间插入一个新节点,那么需要让指针指向插入位置的前驱,然后让新节点的指针指向后继,在让其被连接。
新节点需要先连接后继再被前驱连接,该顺序不能交换。因为节点2是靠节点1找到的,而节点1是遍历过程中自定义的指针指向的。一旦先指向新节点,那么节点2及其后面的所有节点都无法再访问到了。如果选择两个相邻的指针一起后移,使得节点12都被指向,那么顺序就无所谓了。
void Insert_List(List *Head, int n, int e) //在第n位插入数据e
{
List *p = Head;
for (int i = 1; i < n; i++)
p = p->next;
List* node = new List({e, p->next});
p->next = node;
}
带头结点的优势
如果所需要插入的节点是在第一个,含有头结点的单链表依然可以用上面的代码运行不需要用到头指针,而不带头结点的单链表用不到额外的指针,需要用到头指针的。两者的指针不同因此需要用分支来进行判断。
单链表删除节点
删除节点需要用到相邻两个指针,靠右的指针指向被删除的节点,当右指针找到删除节点后,左指针直接指向右指针的后继即可,然后将右指针指向的节点删除。
在插入节点时采用这样的方式也可忽略顺序,最本质的原因是是否会丢失原有的节点。不含头结点的单链表在删除第一个节点时也存在同样的问题,其只需要将头指针移动即可,不需要新指针因此依然需要有分支。下面代码中pre指向的是节点1,p指向头结点,随着循环进行逐步向后查找,以pre为空表示没找到要删除的节点,代码中未给出该情况下的措施,可能会出现删除的第n个节点超过了节点总数。
void Delete_List(List* Head, int n, int &retn) //删除第n位节点并返回值
{
List *pre = Head->next, *p = Head;
for (int i = 1; i < n && pre != NULL; i++)
{
p = pre;
pre = pre->next;
}
p->next = pre->next;
retn = pre->data;
delete pre;
}
单链表查询
获得头结点的地址后第一新的指针往后遍历至NULL即可。
void Print_List(List* Head)
{
List* p = Head->next;
while (p != NULL)
{
cout << p->data << ' ';
p = p->next;
}
puts("");
}
双向链表
插入
双向链表的方式与单链表一样,只是多了往返指针,第一步依然是将插入节点的指针都连上,然后让后继节点的前驱指向新节点,最后前驱节点的后继指向新节点。只要新节点两个指针连接正确,后两步互换依然能找到节点2。
双向指针的连接方式不唯一,节点1与新节点都能通过外部的指针找到,只要确保节点2不会丢失即可。
删除
删除与插入类似,依然时只要确保删除节点后的所有节点不会丢失,顺序有多种。
循环链表
循环链表是将链表最后一个节点的指针指向起始节点,该数据结构可以是单项指针也可以是是双向指针,因此其操作和单双指针类似。
数组模拟链式存储——单链表为例
用数组模拟使用一个数据数组来存放数据,另一个数组来表示后继指针。这两个数组以同一个下标组成一个“结构体”。数据数组内部存放数据与data一样,而指针数组存放下一个节点的下标。因为数组是连续的,可以直接访问下标来获取数据,而结构体链表物理上是不连续的,需要指针记录地址访问。
如图所示,通过访问头指针后将数据依此读取后是4,3,2,1;类似于结构体链表。这里采用从0开始使用数组,在《王道考研》中是不使用的,长度为n的线性表就是1~n+1。
该代码未采用头结点,因此头插法和插入需要两个不同的函数来表示。同样的,根据模拟单链表,剩余两个数据结构也可以根据该代码进行扩展。
#include<iostream>
using namespace std;
const int N = 100;
int a[N],ne[N],index,head;
void init()
{
head=-1,index=0;
}
void inserthead(int x) //头插法
{
a[index]=x;
ne[index]=head;
head=index;
index++;
}
void remove(int k) //删除第k个插入的数后面的数
{
ne[k]=ne[ne[k]];
}
void insert(int k,int x) //表示在第k个插入的数后面插入一个数x
{
a[index]=x;
ne[index]=ne[k];
ne[k]=index;
index++;
}
void print()
{
for(int i=head;i!=-1;i=ne[i]) cout<<a[i]<<' ';
}