线性表的链式存储结构
链式存储结构主要分为单链表、静态链表、循环链表和双向链表四种结构。
在C++标准库中,list是双向链表,forward_list是单向链表,标准库里的容器支持很多的操作,但是这里我自己写的链表只包括链表最基本的操作:获取第i个结点的数值、在第i个结点前插入一个结点、删除第i个结点、头插和尾插法
1.线性表链式存储结构定义
链式存储结构的内存空间是未被占用的任意位置,存储数据的元素信息和它的后继元素的存储地址。
存储直接后继位置的域称为指针域**,存储数据元素信息的域称为**数据域,这两部分信息组成元素ai的存储对象,称为结点(Node)。n个结点链接成一个链表即为线性表的链式存储结构。
2.单链表
如果一个链表的每个节点中只包含一个指针域则这种链表称为单链表。
2.1头指针和头节点和虚拟头结点
头结点:链表的第一个元素的结点。
虚拟头结点(dummyNode)(有时候链表的头结点之前会附设一个结点,称为虚拟头节点):
- 虚拟头节点数据域不存储任何信息,头节点的指针域指向头地址。
- 虚拟头节点是为了操作的统一和方便设立的。
- 有了虚拟头节点,对在头结点前插入结点和删除第一结点,其操作与其他节点的操作就统一了。
头指针(链表指向第一个结点的指针):
- 头指针具有标志作用,链表的名字用头指针命名。
如:ListNode* L;那么L就是头指针,同时L又是链表L的名字。
- 如果没有虚拟头节点,**头指针是指向头结点的指针,**如果有虚拟头节点,头指针是指向虚拟头节点的指针。
- 无论链表是否为空,都必须要有头指针。
2.2单链表结构的代码表示
要想定义单链表结构得先定义链表结点的结构体,然后再定义单链表的结构体:
//单链表结构体
class MyList{
public:
//链表结点结构体
struct ListNode{
int val;
ListNode* next;
ListNode(int val):val(val),next(nullptr){}
};
//默认构造函数
MyList(){
_dummyHead
}
//读取第i个元素的值,类内声明,类外实现
int get(int i);
//在第i个结点前面插入元素e
void insert(int i,int val);
//删除第i个结点
void erase(int i);
//头插法
void push_front(int val);
//尾插法
void push_back(int val);
private:
int _size; //链表长度
ListNode* _dummyHead; //虚拟头节点
};
假设p是指向链表第i个结点的指针,则结点i的数据域为p->data,指针域为p->next,p->next指向第i+1个结点的地址。
2.3单链表的读取
单链表读取的时间复杂度为O(n)。
获得链表第i个数据的算法思路:
- 声明一个指针cur指向链表的头结点,初始化j从0开始(第0个结点即头结点)
- 当j<i时,遍历链表,让cur的指针向后移动,不断指向下一个结点,j累加1
- 否则查找成功,返回结点cur的数据(cur->data)
实现代码算法如下:
// 获取到第i个节点数值,如果i是非法数值直接返回-1, 注意i是从0开始的,第0个节点就是头结点
int MyList::get(int i){
if(i>(_size-1)||i<0){ //若i超出链表范围报错,链表下标是从0开始的,因此大于_size-1
return -1;
}
ListNode*cur=_dummyHead->next; //创建搜寻当前下标,从头节点开始
//下面的循环等价于:
//for(int j=0;j<i;++j){
// cur=cur->next;
//}
while(i--){
cur=cur->next;
}
return cur->val;
}
2.4单链表的插入
假设存储e的结点为s,想要加结点s插入p和p->next之间,不需要移动其他结点,只需要将s->next和p->next的指针做出一点改变即可:
//后给中,中给前
s->next=p->next; //先让s的后继指向p->next
p->next=s; //再让p的后继指向s
//这两步顺序不能颠倒,不然会s的后继将找不到原本的p->next结点。
在有虚拟头结点的情况下,对于表头和表尾元素进行插入和删除的情况,操作是相同的。
- 在第i个节点之前插入一个新节点,例如i为0,那么新插入的节点为链表的新头节点。
- 如果i 等于链表的长度,则说明是新插入的节点为链表的尾结点;如果i大于链表的长度,则返回空
- 声明一个==指针cur指向虚拟头结点,==将cur遍历到第i-1个结点(第i个结点的前一个结点)
- 创建待插入的结点newNode,在cur结点之前插入newNode(后的地址给中的后继,中的地址给前的后继)
void MyList::insert(int i, int val) {
if(i>_size||i<0){
return -1;
}
ListNode newNode=ListNode(val); //待插入的结点
ListNode*cur=_dummyHead;
//下面的循环等价于:
//for(int j=0;j<i;++j){
// cur=cur->next;
//}
while(i--){
cur=cur->next;
}
//后地址给中的后继,中的地址给前的后继
newNode->next=cur->next;
cur->next=newNode;
_size++;
}
2.5单链表的删除
设存储元素ai的结点为q,要想实现将结点q删除单链表的操作,其实就是将它的前继结点的指针绕过,指向它的后继结点即可:
q=p->next;
p->next=q->next; //将q的后继赋值给p的后继。
//即:p->next=p->next->next;
删除第i个结点的算法思路:
- 如果删除的结点i超出链表的范围则直接返回
- 设置一个指针cur指向虚拟头结点,将cur遍历到第i-1个结点的位置
- 将cur的后继结点改成cur的后继的后继结点,即将cur后继的后继的地址传给cur的后继。
// 删除第i个节点,如果i大于等于链表的长度,直接return,注意i是从0开始的
void MyList::delete(int i) {
if (index >= _size || index < 0) {
return;
}
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur ->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
}
2.6顺序线性表和链表的时间复杂度:
- 对于顺序表删除第i个元素,需要先遍历到第i个元素,时间复杂度为O(1);然后再删除第i个元素,由于删除第i个元素需要移动大量元素,时间复杂度为O(n),因此对于顺序,总的时间复杂度为O(n)。
- 对于链表删除第i个元素,需要先遍历到第i个元素,由于线性表的遍历需要遍历i此,时间复杂度为O(n);然后再删除第i个元素,时间复杂度为O(1),因此,总的时间复杂度也为O(n).
线性表的顺序存储结构和单链表的时间复杂度都为O(n).如果我们不知道第i个结点的指针位置,单链表与线性表的顺序存储结构相比没有太大优势。但如果我们想在第i个位置插入10个结点,线性表的顺序存储结构每插入一个结点的时间复杂度都为O(n),但是单链表每次插入一个结点的时间复杂度只有O(1).对于插入或删除越频繁的操作,单链表的效率优势就越明显。
2.7单链表的整表创建(头插法和尾插法实现)
头插法:将新结点插入到虚拟头结点和头结点之间
void MyList::push_front(int val){
ListNode* newNode=new ListNode(val);
newNode->next=_dummyHead->next;
_dummyHead->next=newNode;
_size++;
}
尾插法:将新结点插入到链表的最后
void MyList::push_back(int val){
ListNode* newNode=new ListNode(val);
ListNode* cur=_dummyHead;
while(_size--){
cur=cur->next;
}
cur->next=newNode;
_size++;
}
与顺序存储结构不同,单链表所占空间和位置不需要预先分配,所以创建单链表的过程就是动态生成链表的过程。因此,要用到很多插入的过程,头插法和尾插法是最主要的两种方法
单链表创建的算法思路:
- 初始化链表L,让L的虚拟头结点的指针指向nullptr,并且设置链表长度_size=0._
- 使用头插或者尾插法将新结点插入到链表中。
3.单链表结构和顺序存储结构的优缺点
存储分配方式:
- 顺序结构:用一段连续的存储单元依次存储线性表的数据元素
- 单链表:采用链式存储结构,用一组任意的存储单元存放线性表的元素
时间性能:
- 查找:顺序结构O(1);单链表O(n)
- 插入和删除:
- 顺序存储结构平均移动表长一半的元素,时间复杂度为O(n).
- 单链表在找出位置的指针后,插入和删除时间的复杂度为O(1).
空间性能:
- 顺序结构:需要预分配存储空间,分大了浪费,分小了,溢出
- 单链表:不需要分配存储空间,只要有空间就可以分配,元素个数也不受限制
如:游戏中,对于用户注册的信息,除了注册时插入数据外,绝大多数情况下是读取,所以应该采用顺序存储结构。
游戏中的武器或装备列表,因为会经常扔装备、捡装备会经常换,会随时增加或删除,所以应该用单链表。