数据结构:链表
1.链表定义
链表是一种线性是数据结构,其物理储存空间不一定连续,链表由若干的节点组成,每个节点包含数据域和指针域,数据域存放自定义变量,指针域存放指向下一个节点的指针。
我们通过头指针进入并访问链表,头指针指向第一个节点,即首元节点,为了使每个节点在处理时方式一样,我们也可以人为设置一个头节点(不是必须的)。
头指针与头节点异同如下:
2.单向链表
单向链表是指每个节点的指针域只含一个指针的链表。常用操作包括插入节点,删除节点,头部插入初始化,尾部插入初始化,销毁链表等。下面逐步实现一个带头节点的单向链表。
2.1读取
获得链表的第index
个数据。首先要分析输入的范围,index≥1
且index
不大于表的长度。
具体算法为:
先检验index是否≤0,若成立则输入不合法。
反之:
1.引入Node*
型变量p
获取头指针的指向,引入计数变量i =1
;
2.当计数变量i<index
且p的指针域非空时,计数变量自加1,并将p
的指针域赋值给p
;不断重复此操作(输出第index
个位置的节点的指针p
)
3.判断p是否为空,若为空则说明索引大于表的长度,若非空则输出对应位置的元素
实现代码如下:
void List::GetElem(int index)
{
if (index <= 0)
{
cout << "索引非法!" << endl;
}
else
{
Node* p = head->next;//获取指向首元节点的指针
int i = 1;
while (i < index && p)//未达到索引处且指向当前的指针不为空
{
p = p->next;
i++;
}//输出第index个位置的节点的指针
if (!p)
{
cout << "节点不存在!" << endl;
}
else
{
cout << "第" << index << "个位置的节点为:" << p->data << endl;
}
}
}
2.2节点在指定位置插入
在链表的第index
个数据前插入新元素。首先要分析输入的范围,index≥1
且index
不大于表的长度。
只需要将节点s的指向变为节点p
的指向,并令p
指向s
即可。这两步的顺序不可颠倒,若先令p
指向s
则无法再访问p->next
。操作示意图如下:
算法流程如下:
先检验index是否≤0,若成立则输入不合法。
反之:
1.引入Node*
型变量p
获取头指针的指向,引入计数变量i =1
;
2.当计数变量i<index-1
且p的指针域非空时,计数变量自加1,并将p
的指针域赋值给p
;不断重复此操作(输出第index-1
个位置的节点的指针p
);
3.判断p是否为空,若为空则说明索引大于表的长度,若非空则执行插入操作。
插入操作即先在堆区开辟一个Node*
型变量s
,其指针域用于储存第index-1
个位置的节点的指针p
,即原来第index
个位置的节点的指针,并让p
指向s
,同时完成s
数据域的赋值。
实现代码如下:
void List::Insert(const int index, DataType e)
{
if (index <= 0)
{
cout << "索引非法!" << endl;
}
else
{
Node* s = new Node;//创建一个新节点,p为指向新节点的指针
Node* p = head;//获取头节点
s->data = e;
int i = 1;
while (i < index && p)
{
p = p->next;
i++;
}//输出第index-1个位置的节点的指针
if (!p)
{
cout << "索引过大!" << endl;
}
else
{
s->next = p->next;
p->next = s;
cout << "第" << index << "个位置的节点插入完毕!" << endl;
}
}
}
2.3删除指定位置的节点
下面的q
表示一个变量。
要将节点a(i-1)
绕过节点a(i)
指向下一个节点。可以先用一个Node*
型变量q
储存节点a(i-1)
的指针域,即p->next
,再用p
指向q
的指针域,此时p
的指针域变为p->next->next
,然后释放q
。引入变量q
是必要的,不然无法释放q
对应的内存。
算法思路与插入类似,具体的实现代码如下:
void List::Delete(int index)
{
if (index<=0)
{
cout << "索引非法!" << endl;
}
else
{
Node* p = head;//获取头节点
int i = 1;
while (i < index && p)
{
p = p->next;
i++;
}//输出第index-1个位置的节点的指针
Node* q = p->next;//获取指向第index个位置的指针
p->next = q->next;//将第index+1个位置的指针赋给第index-1个位置的节点的指针域
delete q;//删除第index个位置的节点
cout << "第" << index << "个位置的节点删除完毕!" << endl;
}
}
2.4链表的整表创建
包括头插法和尾插法。
头插法算法流程:
1.在堆区创建一个新节点;
2.新节点指向头指针的指向;
3.头指针指向新创建的节点。
具体实现代码如下:
void List::CreatListHead(int n)
{
for (int i = 0; i < n; i++)
{
Node* p = new Node;
p->data = rand() % 100;
p->next = head->next;
head->next = p;
cout << "第" << i + 1 << "次操作在首元节点前插入了" << p->data << endl;
}
system("pause");
system("cls");
}
尾插法算法流程:
1.在堆区创建一个新节点;
2.新节点指向尾指针的指向,即NULL;
3.尾指针指向新创建的节点。
具体实现代码如下:
void List::CreatListTail(int n)
{
for (int i = 0; i < n; i++)
{
Node* p = new Node;
p->data = rand() % 100;
Node* q = head;
while (q->next)
{
q = q->next;
}//找到指向尾节点的指针q
q->next = p;
p->next = NULL;//p为新的尾节点
cout << "第" << i + 1 << "次操作在尾节点后插入了" << p->data << endl;
}
system("pause");
system("cls");
}
2.5链表的销毁
算法流程如下:
1.声明一个Node*型变量q;
2.q获取头指针的指向,即第1个节点的地址;
3.头指针指向下一个节点;
4.释放q指向的内存;
重复至头指针指向的节点为空。
具体实现代码如下:
void List::ClearList()
{
Node* q;
while (head->next)//跳出循环的条件为头指针指向空
{
q = head->next;//获取头指针指向的节点
head->next = q->next;//头指针指向下一个节点
delete q;//销毁之前的首元节点
}
cout << "销毁完毕!" << endl;
}
3.完整的C++实现
3.1 LinkList.h文件的编写
#ifndef __LINKLIST__
#define __LINKLIST__
//不带头节点的单向链表
#include<iostream>
#include <cstdlib>
using namespace std;
typedef int DataType;
class Node
{
public:
DataType data;
Node* next;
};
class List
{
public:
List();
void GetElem(int index);//读取第index个位置的节点的元素
void Insert(const int index, DataType e);//在第index个位置前插入节点
void Delete(int index);//删除第index个节点
void ShowList();//读取整个链表
void CreatListHead(int n);//在头节点与首元节点间依次插入n个节点
void CreatListTail(int n);//在尾节点后依次插入n个节点
void ClearList();//销毁表
int ListLength();//计算表长
void IsEmpty();
private:
Node* head;//头节点
};
#endif
3.2 LinkList.cpp文件的编写
#include"LinkList.h"
List::List()
{
head = new Node;
head->next = NULL;
}
void List::GetElem(int index)
{
if (index <= 0)
{
cout << "索引非法!" << endl;
}
else
{
Node* p = head->next;//获取指向首元节点的指针
int i = 1;
while (i < index && p)//未达到索引处且指向当前的指针不为空
{
p = p->next;
i++;
}//输出第index个位置的节点的指针
if (!p)
{
cout << "节点不存在!" << endl;
}
else
{
cout << "第" << index << "个位置的节点为:" << p->data << endl;
}
}
}
void List::Insert(const int index, DataType e)
{
if (index <= 0)
{
cout << "索引非法!" << endl;
}
else
{
Node* s = new Node;//创建一个新节点,p为指向新节点的指针
Node* p = head;//获取头节点
s->data = e;
int i = 1;
while (i < index && p)
{
p = p->next;
i++;
}//输出第index-1个位置的节点的指针
if (!p)
{
cout << "索引过大!" << endl;
}
else
{
s->next = p->next;
p->next = s;
cout << "第" << index << "个位置的节点插入完毕!" << endl;
}
}
}
void List::Delete(int index)
{
if (index<=0)
{
cout << "索引非法!" << endl;
}
else
{
Node* p = head;//获取头节点
int i = 1;
while (i < index && p)
{
p = p->next;
i++;
}//输出第index-1个位置的节点的指针
Node* q = p->next;//获取指向第index个位置的指针
p->next = q->next;//将第index+1个位置的指针赋给第index-1个位置的节点的指针域
delete q;//删除第index个位置的节点
cout << "第" << index << "个位置的节点删除完毕!" << endl;
}
}
void List::ShowList()
{
Node* p = head->next;//获取指向首元节点的指针
int i = 1;
while (p)
{
cout << "第" << i << "个节点为:" <<p->data<< endl;
i++;
p = p->next;
}
}
void List::CreatListHead(int n)
{
for (int i = 0; i < n; i++)
{
Node* p = new Node;
p->data = rand() % 100;
p->next = head->next;
head->next = p;
cout << "第" << i + 1 << "次操作在首元节点前插入了" << p->data << endl;
}
system("pause");
system("cls");
}
void List::CreatListTail(int n)
{
for (int i = 0; i < n; i++)
{
Node* p = new Node;
p->data = rand() % 100;
Node* q = head;
while (q->next)
{
q = q->next;
}//找到指向尾节点的指针q
q->next = p;
p->next = NULL;//p为新的尾节点
cout << "第" << i + 1 << "次操作在尾节点后插入了" << p->data << endl;
}
system("pause");
system("cls");
}
void List::ClearList()
{
Node* q;
while (head->next)//跳出循环的条件为头指针指向空
{
q = head->next;//获取头指针指向的节点
head->next = q->next;//头指针指向下一个节点
delete q;//销毁之前的首元节点
}
cout << "销毁完毕!" << endl;
}
int List::ListLength()
{
int i = 0;
Node* p = head->next;
while (p)
{
i++;
p = p->next;
}
return i;
}
void List::IsEmpty()
{
if (head->next)
{
cout << "链表非空" << endl;
}
else
{
cout << "空链表" << endl;
}
}
3.3 main.cpp文件的编写
#include<iostream>
using namespace std;
#include"LinkList.h"
void showLine()
{
cout << "--------------------------------" << endl;
}
int main()
{
List L1;
cout << "头插实验:" << endl;
L1.CreatListHead(5);
L1.ShowList();
cout << "尾插实验:" << endl;
L1.CreatListTail(5);
L1.ShowList();
cout << "元素读取实验:" << endl;
L1.GetElem(8);
L1.GetElem(10);
L1.GetElem(11);
showLine();
cout << "节点删除实验:" << endl;
L1.Delete(2);
L1.Delete(0);
L1.ShowList();
showLine();
cout << "销毁实验:" << endl;
L1.ClearList();
cout<<"表长为:"<<L1.ListLength()<<endl;
L1.ShowList();
L1.IsEmpty();
return 0;
}
4.循环链表
循环链表与单链表的区别是,其尾节点的指针域存着指向头节点的指针,从而形成一个闭环结构。
当链表为空时,判断条件不再是头节点的指向为空,而是头节点的指针指向自己。
4.1尾指针
在单向链表中,访问首元节点的时间复杂度为o(1)
,而访问尾节点的时间复杂度为o(n)
,为了降低访问尾节点的时间复杂度,引入尾指针。
记作Node*
型变量rear
存放一个访问尾节点的指针,那么rear->next
即为头节点,rear->next->next
即为首元节点,访问的时间复杂度均为o(1)
。
4.2循环链表合并
此外,通过引入尾指针,将两个循环链表进行合并时,令A链表的尾节点指向B链表的头节点,令B链表的尾节点指向A链表的头节点,保留链表A的头节点作为合并后的链表的头节点,只需进行如下操作:
1.引入Node*
型变量p
保存rearA->next
,即链表A头节点的地址;
2.A链表的尾节点指向B链表的首元节点;
3.释放链表B头节点;
4.B链表的尾节点指向A链表的头节点,即p;
用代码表示如下:
Node* p = rearA->next;
rearA->next = rearB->next->next;
delete rearB->next;
rearB->next = p;
5.双向链表
5.1普通双向链表
双向链表的指针域中,不仅存放着后继节点的地址,也存放着前驱节点的地址。
非空的双向链表结构如图所示:
当其为空表时,头节点的prior
域与next
域都为空。
5.2循环双向链表
与单向循环链表相似,头节点的prior
指向尾节点,尾节点的next
指向头节点。
其空表为:头节点的前驱与后继节点均指向自己。
双向循环链表具有对称性
p->prior->next = p = p->next->prior;
5.3双向链表节点的插入
先定位节点p
,p为Node*
型变量,存放指向p
节点的地址,在节点p
与前一个节点间插入节点s
。
具体操作如下:
1.s
前驱指向p
的前驱;
2.s
后继指向p
;
3.p
的前驱的后继修改为s
;
4.p
的前驱改为s
。
实现代码如下:
s->prior = p->prior;
s->next = p;
p->prior->next = s;
p->prior = s;
要注意修改p
的前驱的后继时必须在修改p的前驱为s之前。
5.4双向链表节点的删除
先定位节点p
,p为Node*
型变量,存放指向p
节点的地址。将p
的前驱的后继改为p
的后继,将p
的后继的前驱改为p
的前驱,两者次序可交换。之后释放p
即可。
6.小结
但是使用链表要开辟指针域,需要更大的内存存放数据,用空间换时间。