数据结构与算法详解——链表篇(附c++实现代码)

本文深入探讨了链表与数组的内存布局、优缺点比较,重点介绍了链表的插入和删除操作。文章提供了C++实现链表的代码示例,包括list类的定义、构造函数、遍历、插入、删除等方法,并讨论了相关语法问题。
摘要由CSDN通过智能技术生成

链表与数组的内存布局

在这里插入图片描述

  这里假设是在int为4字节的系统上,数组存储在一段连续的内存上,例如图中从地址1000开始,每个格子存放一个4字节的int,就可以通过简单的加法计算出要访问的某一项的地址了,因此数组可以做到随机访问(random-access),也就是任意访问数组某一项a[x]时,可以通过(a的地址+x*sizeof(int))计算得到a[x]地址,时间复杂度是O(1)。
  相反,链表的内存是不连续的,代价就是要存储多一个next指针,指向下一个结点的地址,通过这个地址找到链表的下一个结点,因此链表支持顺序访问(sequential access),也就是当我要访问链表的第x个结点时,我要从头结点开始按顺序遍历next指针才能找到这个结点,时间复杂度是O(n)。

链表与数组的优缺点比较

数组 链表
插入 O(n) O(1)
删除 O(n) O(1)
访问 O(1) O(n)
扩容 O(n) -

  这里还有一个比较重要的点:在遍历数组和链表时时间复杂度都是O(n),但是实际上数组耗时比链表小很多,原因是cpu缓存对连续内存的数组友好,对不连续内存的链表不友好。
  计算机的存储器分6个层级结构:(处理速度从快到慢)

  • 寄存器
  • 高速缓存 (cpu缓存,一般有L1,L2,L3三层缓存)
  • 主存储器(内存)
  • 磁盘缓存
  • 固定磁盘
  • 可移动存储介质

  根据局部性原理,cpu在读取内存时会读取一片连续空间的内存,将一些经常访问的数据放在cpu缓存,在访问数据的时候,先检查缓存里是否存在,如果存在就直接读取使用,如果不存在再去内存读取。CPU缓存的访问速度和内存的访问速度可能相差几十倍。

数组的插入和删除

  数组进行插入操作时,要把插入的位置打后的所有元素往后移,把插入的位置腾出来,所以需要O(n)的时间复杂度,代码表示:

void insert(int index,int data){
   
//size表示数组中存储的元素数量,capacity表示容量即new的时候申请的空间大小
	if(size==capacity)	
		resize();
	for(int i=size-1;i>=index;i--)
		array[i+1]=array[i];
	array[index]=data;
	++size;
}

  同理,数组进行删除时,需要把删除的位置打后的所有元素往前移,同样是需要O(n)的时间复杂度。
  访问在上面内存布局中已经讲述,这里不再复述。
  这里还有个扩容的操作,也就是当申请的空间不够用时,需要重新申请一块新的空间,然后将旧空间的元素拷贝到新空间上去,然后释放掉旧空间的内存,也是需要O(n)的时间复杂度。

链表的插入删除

在这里插入图片描述
  链表的插入和删除就是指针的替换,下面我用伪代码表示

//插入,newNode表示要插入的结点(上图中2的结点),在preNode(上图中1的结点)后面插入
Node* newNode=new Node(data);	
newNode->next=preNode->next;	//上图2指向4的那个箭头
preNode->next=newNode;			//上图1指向2的那个箭头

  但是这里有几个问题:
  1、preNode如何得到?
  答:preNode就是一开始我们在内存布局中说到的从头结点开始顺序访问next得到的,比如要在第2个元素后插入,那么就遍历到第二个真正存储数据结点,所以如果单纯只是算这几个指针替换的时间复杂度的话就是O(1),但是要完成整个删除操作需要顺序访问,时间复杂度是O(n)。
  2、如果一开始链表没有任何结点为空时,上述插入逻辑就行不通了,所以要打个补丁:

if(head==nullptr)
	head=newNode;

  3、那如果我想在头结点前插入呢,也就是让插入的结点成为新的头结点,还要再打个补丁:

Node* oldhead=head;
head=newNode;
head->next=oldhead;

  针对问题2和3,我们可以通过加入一个哨兵解决,让插入的逻辑都统一成第一段插入代码那样。
  我们可以让头结点变成一个哨兵,不存储数据,真正存储数据的第一个结点是head->next,也就是初始化时head不赋值为nullptr,而是创建一个新结点,这样就能让我们的插入和删除操作逻辑统一起来,后面有实例代码。

  删除也是同理,这里就不再过多描述,伪代码:

//删除,delNode表示要插入的结点(上图中2的结点),在preNode(上图中1的结点)后面删除
preNode->next=delNode->next;	//上图1指向4的那个箭头
delete delNode;

  至于扩容,链表天然就支持扩容,不用像数组一样预先申请一大段空间,而是需要时才申请一个结点的空间,链表的扩容已经融合在插入操作中了。

c++代码示例

  在这里我实现了一个带头结点(哨兵)尾结点(非哨兵)双向列表,相对来说比较全面一点,如果你只需要单向列表,就把prev指针的逻辑删除掉就好,不需要尾结点就把tail的逻辑删除掉就好。

list类的定义

// list:双向链表
template<typename DataType>
class list {
   
private:
	struct Node {
   		//struct默认是public,class默认是private,可以用class和friend,也可以class Node元素掌握设置为public
		DataType data;
		Node* prev;
		Node* next;
		Node() {
   }
		Node(const DataType& d) :data{
    d }, prev{
    nullptr }, next{
   nullptr} {
   }
		//这里不需要delete prev和next因为它们不是new出来的,否则会无限循环调用~Node(),这里可以不用写这个析构函数
		//~Node() { std::cout << "~Node()" << std::endl;}
	};

private:
	Node* head;	//哨兵,不存储值
	Node* tail;		//尾部,非哨兵
	int size;

public:
	list();
	list(const list<DataType>& ls);
	list(std::initializer_list<DataType> init_list);
	~list();
	const list<DataType>& operator=(const list<DataType>& ls);
	DataType& operator[](int index);	//返回DataType&,那么ls[n]可以赋值,比如ls[2]=xxx
	DataType operator[](int index) const;	//返回DataType,那么ls[n]不可以赋值,后面带const让const list对象有一个可以调用的版本
	void insert(const DataType data, int pos);
	void remove(const DataType data);
	void remove_at(int index);
	void append(DataType data);		//链表尾部追加
	void printList() const;
	bool empty();
	void clear();
	int length() const;

private:
	Node* get(int index) const;	//因为operator[]() const里会调用get,所以get后面也要有const
	Node* search(DataType data);
	void append(Node* node);
	void remove(Node* node);
};

构造函数

template<typename DataType>
list<DataType>::list() {
   
	head = new Node();
	head->prev = head->next = nullptr;
	tail = head;
	size = 0;
}

  上面说哨兵时也有说到,head在初始化时就会创建,但是不存储值,注意tail不是哨兵,但是初始化时也将head赋值给tail,为后面写插入删除等操作时方便。

template<typename DataType>
list<DataType>::list(std::initializer_list<DataType> init_list) :list() {
   //委托list()初始化
	for (auto e : init_list) 
		append(e);
}

  这里使用了c++11的初始化列表std::initializer_list,那么在使用时就可以用list ls = { 1,2,3,4 }这种方式进行初始化。

顺序访问(遍历)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值