系列文章目录
第一章 【数据结构C++】线性表/顺序表-数组与vector
第二章 【数据结构C++】线性表/顺序表-数据类型、增删改查操作
第三章 【数据结构C++】线性表/顺序表-实战:通信录
第四章 【数据结构C++】线性表-链式存储:链表类型和单链表(定义+代码实现)
第五章 【数据结构C++】线性表-链式存储:双链表、循环链表、静态链表(定义+代码实现)
文章目录
前言
我们用前三章内容介绍了线性表中的顺序存储的内容。我们知道,顺序表的优点是可随机存储且存储密度高,但是也存在不足:要求大片连续空间,改变容量不容易,插入和删除需要时间复杂度为O(N)。本章我们介绍线性表的链式存储——链表。
链表优点:不要求大片连续空间,改变容量方便;
缺点:不可随机存取,要耗费一定空间存放指针。
一、链表类型
四大类:单链表、双链表、循环链表、静态链表。
链表特点:将每个结点放在一个独立的存储单元中,结点间的逻辑关系依靠存储单元中附加的指针来给出。结点的存储单元在物理位置上可以相邻,也可以不相邻。简而言之,链表中各结点不一定放在连续的存储空间,而是依靠指针把他们连接在一起。
二、单链表
1. 存储结构
单链表中各个节点的存储结构如下:
2. 两种实现形式:带头结点和不带头结点
单链表分为带头结点和不带头结点两种。首先我们了解头指针、头结点和首元结点的含义。
- 头指针:指向链表中第一个结点(有头结点的指向头结点,无头结点则指向首元结点)的指针。单链表可由一个头指针唯一确定,能够标识一个单链表,也常做链表的名字。
- 头结点:在链表的首元结点之前附设的一个结点;数据域内只放空表标志,表长等信息或者是空的;也可做监视哨。
- 首元结点:指链表中存储线性表第一个数据元素的结点,也称为第一元素结点。
不带头结点的单链表: 头指针指向第一个数据元素结点。
若链表为空,则只有头指针指向NULL:head->NULL
。
带头结点的单链表: 指向空的头节点。
若链表为空,则只有头指针指向空的头结点。
头结点的意义:使得在表头位置上进行插入和删除和在其它非空结点位置上是完全一致的,从而使得插入和删除算法得到简化。
3. 类型定义(代码)
代码模板(伪代码)如下:
template <class elemType>
struct Node {
public:
elemType data; // 数据域
Node* next; //指针域
Node(const elemType value, Node* p= NULL){//两个参数的构造函数
data = value;
next = p;
}
Node(Node*p=NULL){//一个参数的构造函数
next = p;
}
};
此处插播一则关于C++中struct
和typedef
的语法小知识:
struct
是声明一个结构体,它可以将不同类型的数据存放在一起,作为一个整体进行处理。例如:结点数据域data
是elemType
类型,指针域next
是Node*
类型,这俩个数据类型不同但是他们又是表示一个整体(单链表),所以用结构体struct把他们结合起来,形成一个新的数据类型。struct
的声明需要放在main函数之前。
typedef
:为类型取一个新的名字,例如:typedef int zhengshu;
意思是将int
类型换一个新名字叫zhengshu
。前面讲到指针域next
是Node*
类型,我们需要创建新结点时,内存中申请一个结点所需空间,并用指针p指向这个结点:
struct Node* p= (struct Node*) malloc (sizeof(struct Node*));
我们写了三次struct Node*
,很麻烦,所以我们用typedef
重命名Node*
:
格式:typedef <eleType> <别名>
例子:typedef int zhengshu;
旧:int x=1;
新:zhengshu x=1;
回归单链表定义:
typedef struct Node Node;
新: Node* p= (Node*) malloc (sizeof(Node*));
具体定义代码:
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个节点存放一个数据元素
struct LNode *next; //指针指向下一个节点
}LNode, *LinkList;
这相当于:
typedef struct LNode LNode;
typedef struct LNode *LinkList;
4. 创建一个结点(代码)
要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点。
LNode* L;
//声明一个指针,该指针指向单链表第一个结点,强调返回一个结点
或
LinkList L;
//声明一个指向单链表第一个结点的指针,强调返回一个单链表
若想在堆区开辟空间时,C是使用malloc函数,而在C++则是使用new关键字。利用C++语言的new和delete操作符给对象分配和释放空间:
Node<elemType>* p = new Node<elemType> (value,NULL);
p的类型为Node<elemType>*
型,所以该无名结点可以用指针p间接引用,数据域为(*p).data
或p->data
,指针域为(*p).next
或p->next
。
5. 单链表完整定义模板(代码)
代码如下:
template <class elemType> //elemType为单链表存储的元素类型
class linkList: public List<elemType>{
private:
struct Node { // 结点类型
public:
elemType data;//结点的数据域
Node* next; //结点的指针域
Node(const elemType value, Node* p= NULL){ // 两个参构造函数
data = value;
next = p;
}
Node(Node* p= NULL){ //一个参构造函数
next = p;
}
};
Node* head; //单链表的头指针
Node* tail; // 单链表的尾指针
int curLength; //单链表的当前长度,牺牲空间换时间
Node* getPosition(int i)const;// 返回指向位序为i的元素的指针
public:
linkList();// 构造函数
~linkList();// 析构函数
void clear();// 将单链表清空
bool empty()const{ return head->next==NULL;}// 判空
int size()const{ return curLength;}// 返回单链表的当前实际长度
void insert(int i,const elemType &value);// 在位序i上插入一个元素value
void remove(int i);// 删除位序i上的元素
int search(const elemType&value)const;//查找值为value的元素第一次出现的位序
int prior(const elemType&value)const;// 查找值为value的元素的前驱的位序
elemType visit(int i)const;// 访问位序为i的元素值,0定位到首元结点
void traverse()const;// 遍历单链表
void headCreate();//“头插法“创建单链表
void tailCreate()//“尾插法“创建单链表
void inverse();// 逆置单链表
};
尾指针的创建:尾指针的加入使得频繁修改表尾结点的值或后继时,无需从头遍历链表。
5. 各函数模板(代码)
5.1 初始化链表
template <class elemType>
linkList<elemType>::linkList(){
head = tail = new Node(NULL);// 创建带有头结点的空表
curLength=0;
}
//不带头结点的
template <class elemType>
linkList<elemType>::linkList(){
head=NULL;// 创建不带有头结点的空表
curLength=0;
}
5.2 析构函数
时间复杂度为O(N) :
template <class elemType>
linkList<elemType>::~linkList(){
clear(); // 清空单链表
delete head; // 释放头结点
}
5.3 清空链表
主要操作是将工作指针从头结点一直移动到链表尾,边移动指针边释放结点。时间复杂度为O(N) :
template <class elemType>
void linkList<elemType>::clear(){
Node * p,* tmp;// p为工作指针,指向首元结点
p= head->next;//引入工作指针是为了防止随意修改头指针
while(p != NULL){// 等效while(p)
tmp= p;
p= p->next;// 指针后移
delete tmp;
}
head->next=NULL;// 头结点的指针域置空
tail = head;// 头尾指针均指向头结点
curLength=0;
}
5.4 求表长
时间复杂度为O(1) :
template <class elemType>
int linkList<elemType>::size()const{
return curength;//直接返回curLength
}
//若没有curLength O(N)
template <class elemType>
int linkList<elemType>::size()const{ //若没有curLength
Node *p =head->next;// 需要从头到尾遍历链
int count=0;
while(p){ count++;p=p->next;}
return count;
}
5.5 遍历链表
时间复杂度:O(N)
template <cass elemType>
void linkList<elemType> ::traverse()const{
Node *p= head->next;// 工作指针p指向首元结点
cout << "traverse:";
while(p != NULL){
cout << p->data <<" ";
p = p->next; // 向后移动指
}
cout << endl;
}
5.6 查找位序i的元素 O(N)
template <class elemType>
typename linkList<elemType> :: Node* linkList<elemType> ::getPosition(int i)const {
if(i<-1 || i>curLength-1) // 合法查找位置为[-1..n-1]
return NULL; //当i非法时返回NULL
Node *p= head; // 工作指针p指向头结点
int count = 0;
while(count <=i){
p = p-> next;
count++;
}
return p; // 返回指向位序为i的结点的指针
}
5.7 查找值为value的元素的位序 O(N)
emplate <class elemType>
int linkList<elemType> ::search(const elemType&value)const{
Node*p= head->next;//工作指针p指向首元结点
int count = 0;// 首元结点的位序为0
while(p != NULL && p->data != value){
p = p->next;
count++;
}
if(p== NULL){
return -1; //查找失败返回-1,这里-1并非头结点
}else{
return count; // 查找成功,count为元素的位序
}
}
5.8 查找值为value的元素的前驱的位序
思想:求值为value的元素的前驱,需要从链表的第一个结点开始遍历链表。我们设置两个指针p和pre,分别指向当前正在访问的结点和它的前驱结点,还需要一个计数器count从链表的第一个结点开始遍历链表。
(1)若p== NULL,则查找值为value的元素失败,返回-1.
(2)若查找值为value的元素成功,且该元素是首元结点则无前驱,返回-1;
(3)若查找值为value的元素成功,且该元素不是首元结点,则返回其前驱的位序。
时间复杂度为O(n)。
template <class elemType>
int linkList<elemType> ::prior(const elemType&value)const{
Node *p= head->next;//p是工作指针指向首元结点
Node *pre = NULL;// pre指向p的前驱结点
int count= -1;// 注意:-1表示首元结点无前驱
while(p &&p->data != value){
pre = p;// 前驱指针后移
p = p->next;// 指向下个待处理结点
count++;
}
if(p== NULL) return -1;//查找失败返回-1,这里-1并非头结点
else return count;//查找成功,count为元素的位序
}
5.9 插入元素 O(N)
template <class elemType>
void linkList<elemType> :: insert(int i,const elemType &value){
Node *p,*q;
if(i<0 || i> curLength) //合法的插入位置为[0..n]
throw outOfRange(); // 插入位置非法,抛出异常
p= getPosition(i-1);// P是位序为i的结点的前驱
q= new Node(value,p->next);// 申请新结点q
p->next = q;// q结点插入到p结点的后面
if(p == tail)
tail = q;// 若插入点在表尾,q成为新的表尾
curLength++;
}
5.10 删除元素 O(N)
template <class elemType>
void linkList<elemType>::remove(int i){
Node *pre,*p;// p是待删结点,pre是其前驱
if(i<0 || i> curLength-1)//合法的删除位置为[0..n-1]
throw outOfRange();//当待删结点不存在时,抛出异常
pre= getPosition(i-1);
p = pre->next; // P是真正待删结点
if(p == tail){// 待删结点为表尾结点,则修改尾指针
tail = pre;
pre->next=NULL;
}else{
pre->next=p->next;
}
delete p;
curLength--;
}
5.11 常用——头插法
作用:创建单链表、逆置 。
时间复杂度为O(n) 。
头插法是在链表的头部插入结点建立单链表,也就是每次将新增结点插入在头结点之后,首元结点之前。
图中显示了根据线性表(5,4,3,2,1)创建带有头结点的单链表的过程,因为是在链表的头部插入,所以读入数据的顺序为1,2,3,4,5和线性表中的逻辑顺序是相反的。
// 头插法创建单链表
template <class elemType>
void linkList<elemType>:: headCreate(){
Node *p;
elemType value,flag;
cout<<"input elements,ended with:";
cin>>flag;// 输入结束标志
while(cin>>value,value != flag){
//创建新结点: p->data=value,p->next= head->next;
p= new Node(value,head->next);
head->next= p;// 结点p插入到头结点的后面
if(head == tail) tail = p;// 原链表为空,新结点p成为表尾结点
curLength++;
}
}
5.12 常用——尾插法 O(N)
尾插法是在链表的尾部插入结点建立单链表,tail指针在这里起作用。
template <class elemType>
void linkList<elemType> ::tailCreate(){// 尾插法创建链表
Node *p;
elemType value,flag;
cout<<"input elements,ended with:";
cin>>flag; // 输入结束标志
while(cin>>value,value!=flag){
p=new Node(value,NULL);
tail->next=p; // 结点p插入到表尾结点的后面
tail=p; // 结点p成为新的表尾
curLength++;
}
}
5.13 逆置单链表
工作指针p依次访问链表中的每个结点,每访问一个结点,将它插入到头结点的后面(头插法)。
时间复杂度为O(n)。
template <class elemType>
void linkList<elemType> :: inverse(){// 头插法逆置
Node *p,*tmp;
p=head->next; // p为工作指针指向首元结点
head->next=NULL; // 头结点的指针域置空,构成空链表
if(p) tail=p; // 逆置后,原首元结点将变成表尾结点
while(p){
tmp=p->next; // 暂存p的后继
p->next=head->next;
head->next=p; // 结点p插入到头结点的后面
p=tmp; // 继续处理下一个结点
}
}
5.14 合并链表
非递减有序的单链表la和lb合并成新的非递减有序单链表lc,要求利用原表空间。
算法思想:因为新创建的单链表lc仍然是非递减有序的,所以用尾插法创建lc表。
template <class elemType>
typename linkList<elemType> * linkList<elemType> ::Union(linkList<elemType> * lb){
Node *pa,*pb,*pc; // 分别是链表la、lb、lc的工作指针
linkList<elemType>* lc = this; // lc表利用la表空间
pa=head->next;
head->next=NULL; // la表构成空链表
pb=(lb->head)->next;
(lb->head)->next=NULL;// lb表构成空链表
pc=lc->head; // lc表直接利用la表头结点
while(pa && pb){ // la和lb均非空
if(pa->data<=pb->data) { // pa所指结点尾插法插入lc表
pc->next=pa; pc=pa; pa=pa->next;
}else{ // pb所指结点尾插法插入lc表
pc->next=pb; pc=pb; pb=pb->next;
}
}
if(pa){ // 若pa未到尾,将pc指向pa
pc->next=pa;
lc->tail=tail; // 修改尾指针,因lc=la,这条语句可省略
}else{
pc->next=pb; // 若pb未到尾,将pc指向pb
lc->tail=lb->tail; // 修改尾指针
}
lc->curLength = curLength+lb->curLength;
delete lb;
return lc;
}
总结
-
链表的特点:
-
单链表时间复杂度的总结:
查找:不支持随机存取,O(N).
插入和删除:遍历链表,找到代操作结点的前驱结点,通过修改指针完成。O(N)+O(1)=O(N). -
来自链接文章的总结表格。总结得很清晰,供大家一起学习。