数据结构——链表

【基本知识】

链表是一种常见的数据结构,用于存储和组织数据。它由一系列节点组成,每个节点包含两部分:数据(data)和指向下一个节点的指针(*next)。链表中的节点可以在内存中不连续地分布,通过指针将它们连接起来。

链表有多种类型,其中最常见的是单向链表和双向链表。在单向链表中,每个节点只有一个指针指向下一个节点;而在双向链表中,每个节点有两个指针,分别指向前一个节点和后一个节点。

链表的优点是插入和删除操作的效率较高,因为只需要修改指针的指向即可,而不需要移动其他元素。然而,链表的缺点是访问特定位置的元素时效率较低,需要从头节点开始遍历。和顺序表(数组)相比,链表插入元素的时间复杂度为O(1),访问特定位置元素的时间复杂度为O(n)。

数组链表
插入元素O(n)O(1)
访问特定元素O(1)O(n)

作为数据结构的一种,链表的常用操作同样包括增,删,改,查,如下:

        1.增:即插入新节点,输入新元素进入链表;

        2.删:从链表中删除指定位置的节点;

        3.改:可以修改链表中某个节点的值

        4.查:可以根据节点的值或索引查找链表中的节点

【代码解析】

创建节点:

每个节点中需要包含数据(data)和指向下一个节点的指针(*next)两部分;创建节点,将节点初始化,将输入值x传入节点的data。

宏定义一个元素类型eleType,便于更改链表的类型。

#include <iostream>

#define eleType int

using namespace std;

struct ListNode {
	eleType data;
	ListNode* next;

	ListNode(eleType x): data(x),next(NULL) {} //创建链表节点;初始化链表,把x赋值给data
};

创建链表:

创建一个LinkedList类,其中私有部分定义出链表的头节点head和链表空间大小size;共有部分声明一些需要使用到的函数。

class LinkedList {
private:
	ListNode* head;
	eleType size; //单向链表空间大小

public:
	LinkedList():head(NULL),size(0) {}		//链表创建函数
	~LinkedList();                          //创建析构函数删除缓存
	void insert(int i, eleType val);		//插入链表函数
	void remove(int i);						//删除链表节点函数
	ListNode* find(eleType val);			//查找函数
	ListNode* get(int i);					//获取链表
	void update(int i, eleType val);		//更新链表
	void print();							//打印链表

};

插入新节点:

定义插入节点函数,类型为空(void),包括元素索引(i)、元素数据Validation(val);

首先判断插入的节点位置是否合法,如果1.在头节点之前(即i<0) 2.超出链表内存范围(即i>size)则不合法,直接报错,在文件开头引用新的头文件

#include <stdexcept>

<stdexcept>是C++标准库中的一个头文件,它定义了一些标准的异常类,这些异常类都继承自std::exception类。它提供了一些用于处理异常的工具和机制。在C++中,当程序发生错误或异常时,可以通过抛出异常来通知上层代码,然后在合适的地方进行异常处理。而<stdexcept>中定义的异常类可以用于抛出和捕获一些常见的异常情况,例如逻辑错误(std::logic_error)、运行时错误(std::runtime_error)等。这些异常类可以帮助开发人员更好地识别和处理各种异常情况,从而提高程序的可靠性和健壮性。

若位置合法,则生成新节点:用new关键字新建一个实例对象ListNode,用于储存新插入的元素;

将插入位置分情况讨论:

1.插入位置在开头节点,则将head的值赋给newNode指向的next,将新元素数值赋给head(通俗来说,newNode->next就是新节点的下一节点,把原本头位置0号位的元素放到新插入的元素后一个位置,即1号位,再把新元素放到0号位)

2.插入位置不在开头节点时,也是同样的道理;这里我们需要一个游离指针curr来临时存放元素,类似于C++中a,b交换值时定义的第三变量temp;游离指针指向头节点的位置(便于后续遍历的操作)

先把curr的位置指向想插入节点的前一位置,将curr->next赋值给newNode->next(也就是把需要插入元素的位置的节点的值赋给newNode的下一个节点,需要插入位置的节点就是游离指针的下一个位置,即curr->next,与newNode是同一个位置),总的来说就是把要插入位置的节点的后续节点全部向后移动一位

举个例子:比如你想在三号位置处插入一个新的节点,则原先三号位和 后续的节点需要全部向后移动一位。先将游离节点遍历到二号位,它的next指针则指向三号位,由于原本的三号位置需要向后移动一位,则把它的值传给newNode的下一位,即newNode->next(此时指向四号位),将新的元素newNode赋给curr的next,即curr->next(此时指向三号位)。

最后,由于插入了新的节点,则链表的内存空间需要加一。

void LinkedList::insert(int i, eleType val) {
	if (i<0 || i>size) {
		throw std::out_of_range("Invalid position");	//小于0或超出内存范围报错
	}
	ListNode* newNode = new ListNode(val);				//生成新节点储存插入元素
	if (i == 0) 
       {										//插入头节点
		newNode->next = head;							//头节点的值赋给新节点的下一位置
		head = newNode;									//将新元素数值赋给head
	   }
	else 
       {
		ListNode* curr = head;							//定义游标节点,从head开始遍历
                    
                            //遍历i-1次,使curr等于后继next,即curr为插入结点的前一个位置
		for (int j = 0; j < i - 1; j++)
		{ curr = curr->next; }	
                            //遍历i-1次,使curr等于后继next,即curr为插入结点的前一个位置
		newNode->next = curr->next;
		curr->next = newNode;    
		
	   }
	++size;												//储存空间加一
	}

删除节点:

和插入节点相同,首先先检查其位置合法性,若合法才进行后续的删除操作;

同样分开讨论:

1.删除头位置的节点时,先创建一个temp指针存放head,head的下一节点head->next将值传给head,相当于向前移一位,最后删除存储在temp中原来的head数据;

2.删除其他位置的节点时,同样需要创建一个游离指针来存放缓存数据,将其遍历到需要删除节点的前一位置,因为我们需要找到要移除节点的前一个节点,然后创建一个指针 temp,指向要移除的节点 ,即curr->next,将 curr 的 next 指针指向 temp 的下一个节点,从而跳过要移除的节点。最后,通过 delete 关键字释放临时指针 temp 指向的内存,从而删除要移除的节点。

void LinkedList::remove(int i) {
	if (i<0 || i>=size) {
		throw std::out_of_range("Invalid position");	//小于0或超出内存范围报错
	}
	if (i == 0) {
		ListNode* temp = head;
		head = head->next;
		delete temp;
	}
	else{
		ListNode* curr = head;
		for (int j = 0; j < i -1; j++)
		{
			curr = curr->next;
		}
			ListNode* temp = curr->next;
			curr->next=temp->next;
			delete temp;
	}
	--size;
}

查找元素:

这段代码用于查找链表中包含特定值的节点:

首先创建一个指向链表头节点的指针 curr,用于遍历链表。

遍历链表,循环的条件是 curr 不为空且当前节点的值不等于要查找的值 val;换句话说就是直到找到包含特定值的节点或者遍历到链表的末尾才会停止循环。在每次迭代中,通过 curr->next 将 curr 移动到链表中的下一个节点。

循环结束后,函数返回指针 curr。如果找到了包含特定值的节点,则返回指向该节点的指针;如果没有找到匹配的节点,返回空指针。

ListNode* LinkedList::find(eleType val) {
	ListNode* curr = head;
	while (curr && curr->data != val) {
		curr = curr->next;
	}
	return curr;
}

这段代码用于获取链表中指定位置的节点元素值:

由于要接受一个索引 i,则需要判断其合法性,是否小于 0 或大于等于链表的大小 size。如果是,它会抛出一个异常,表示位置无效

和前面一样,定义一个游离指针curr,使用循环将指针 curr 移动到指定位置的节点,在每次迭代中,通过 curr->next 将 curr 移动到链表中的下一个节点。直到移动到所查找的节点时停止循环,返回该节点的值。

ListNode* LinkedList::get(int i) {
	if (i < 0 || i >= size) {
		throw std::out_of_range("Invalid position");	//小于0或超出内存范围报错
	}
	ListNode* curr = head;
	for (int j=0 ; j < i; j++)
	{
		curr = curr->next;
	}
	return curr;
}

修改元素:

由于前面已经创建了get(i)函数,这里可以直接调用;查找出该节点,将更新的值val赋值给get()返回的curr(指向当前位置i)的data。

void LinkedList::update(int i, eleType val)
{
	get(i)->data = val;
}

遍历链表并打印:

先创建一个指向链表头节点的指针 curr,用于遍历链表,当 curr 不为空,即还有节点时重复遍历。

在循环的每次迭代中,使用 cout 对象打印当前节点 curr 的值 curr->data,并在值后面加上一个空格。然后,通过 curr->next 将 curr 移动到链表中的下一个节点。

void LinkedList::print() {
	ListNode* curr = head;
	while (curr) {
		cout << curr->data << " ";
		curr = curr->next;
	}
	cout << endl;
}

【运行结果】

在main函数中创建一个名为list的链表,插入若干节点,打印链表;

将3节点更新为100,打印;

删除2节点,打印。

int main() {
	LinkedList list;		//创建一个链表名称为list
	list.insert(0, 0);
	list.insert(1, 1);
	list.insert(2, 2);
	list.insert(3, 3);
	list.insert(4, 4);
    list.print();

	list.update(3, 100);
	list.print();

	list.remove(2);
	list.print();
}

【总结】

在该结构的运用中,最重要的是节点的遍历:

curr=curr -> next 相当于 i +=1 ,为向后遍历

curr -> next=curr 相当于 i -=1 ,为向前遍历

同样的,节点的向后(前)移动也是这个道理:newNode -> next = curr -> next 

最后,理解不了的地方要多画图!!!

  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值