链表是通过 分布在内存各个位置上不同节点相互连接成的一种线性表的数据结构,其特性突出表现为 内存分布零散,因此在数据存储对内存要求相较于数组而言较低。
链表的时间复杂度
- 数据插入:链表的数据的内存由于是分散的,因此在插入过程中只需要考虑前后数据指针的改变即可,不用考虑插入节点后的数据迁移工作,因此链表单纯的插入操作的时间复杂度是O(1),但是因为数据的查找访问需要遍历整个链表,因此在链表中任意位置插入节点数据时,仅仅是遍历占用的时间复杂度为O(n),仅仅在头节点插入时不需要遍历此时的时间复杂度才为O(1)。
- 数据删除:与数据插入一样,数据节点的删除只要考虑删除节点前后两节点指针的变化,因此链表节点单纯删除的时间复杂度是O(1),与插入的情况基本一致,删除头部首个数据节点时时间复杂度才为O(1),其余均为O(n)。
- 数据访问:由于链表的特性是非连续内存分布的数据结构,因此不具备随机访问的特性,在查找、数据访问时需要进行遍历链表的操作,因此链表的数据访问操作的时间复杂度为O(n)。
单向链表与双向链表
单向链表:链表结构中只有一个next指针,仅仅实现单向传递
双向链表:链表结构中含有两个指针,prev和next指针,能够前后照应
单向链表与双向链表的比较
-
在插入与删除节点的操作过程中,假设需要操作的节点是链表中间某个节点,此时左右相邻的两个节点都是存有数据的节点,并非头尾两节点,假设需要操作的是在指定节点的前面进行插入或者删除,此时对于单向链表而言,仅仅知道指定节点后的那一个节点,因此,还需知道前一个节点才能进行指针的赋值操作,因此还需进行一次遍历操作至指定节点的前一个节点并记录其信息,在此情况下,节点的插入或者删除的时间复杂度为O(n),但是由于双向链表的前后指针的特殊性使得在此需求下的时间复杂度变为O(1)
-
当链表为有序状态下时,双向链表的查找比单向链表的效率高一些,因为是双向的,所以可以决定是从头开始还是从尾开始,再加上有序的前提,所以平均下来双向链表只需要遍历一半的数据。
-
综上所述,双向链表比单向链表更加高效,但并不是采取一刀切的方式选取双向链表来应对需求。当内存空间很富裕,倘若追求极致的代码运行速度,则可以选择双向链表这种空间复杂度稍高,时间复杂度稍低的数据结构,以空间换取时间;但是在内存空间不充裕的情况下,则需要忽略掉部分运行速度,使得程序能够安全运行,以时间换空间
链表与数组的比较
- 数组与链表采取截然不同的内存组织方式,因此针对不同的需求需要采取不同的使用策略
- 数组简单易用,使用的是连续的内存空间,利用CPU的缓存机制去预读数组中的数据,随机访问的效率更高,链表的空间零散,对于CPU缓存而言不太友好。
- 数组的大小固定,尤其当数组的大小特别大时,容易出现内存不足的情况,并且突然改变数组大小后的数据拷贝工作也极其费时。但链表不一样,链表依托其分散内存空间的特性,能够极大程度减少出现内存不足的现象。
- 与数组相比,链表更适合用于数据的插入、删除操作频繁的场景,数组更适合用于随机访问频繁的场景,当然并不是照本宣科还是要依照实际情况进行分析
单向链表的C++实现
#pragma once
#include<iostream>
typedef struct Node
{
int value;
Node* next;
}node;
class Linklist
{
private:
node* head;
int size;
public:
//默认构造函数
Linklist();
//插入节点,尾插法
void AddNode(node* n);
//insert采用尾插法,插入pos节点的后面
void Insert(int pos, node* n);
//删除pos后的节点
void DelNode(int pos);
//获取当前容量
int GetLinklistSize();
//查找指定元素的位置
void FindNode(int value);
//释放空间
void FreeNode();
//打印链表内容
void PrintLinklist();
//反转链表
void ReverseLinklist();
};
#include"linklist.h"
#include<iostream>
using std::cout;
using std::endl;
Linklist::Linklist()
{
this->size = 0;
this->head = new node;
this->head->value = 0;
this->head->next = NULL;
}
void Linklist::AddNode(node* n)
{
node* temp = new node