数据结构解析——小白也能看懂的单链表

引言

单链表在数据结构中是很重要的一种线性结构,它是单向的,有着非常广泛的应用领域;虽然现在很多语言中都有封装好的链表类型可以直接使用,但是自己能写一个链表并实现基本操作是至关重要的;

接下来我将用代码展示单链表的创建和一些基本操作;
注:以下代码仅供参考,并不一定是最优代码,只是想让各位了解单链表如何进行的一些基本操作;

单链表的结构

单链表就是由一个一个节点组成,这个节点由一个数据域和指针域组成;
如图:
在这里插入图片描述
所以,我们需要先创建节点结构,然后才能依次组成单链表;
注:以下链表的数据域的数据类型都是int类型,实际情况可以修改为任意数据类型;

这里使用结构体来创建,代码如下:

// 单链表节点结构
typedef struct Node {
	int data; // 数据域
	struct Node* next; // 指针域
}NODE;

可以看到,指针域指向的其实就是该节点本身的数据类型,所以这一点一定要注意!

单链表的初始化和创建

有了基本组成单元,那么就要创建链表了,这里我分为两个函数来实现:

  • void initList(NODE*& head); // 初始化链表
  • void creatList(NODE*& head); // 创建一个链表

初始化链表很简单,直接看代码:

// 初始化链表
void initList(NODE*& head) {
	try {
		head = new NODE;
	}
	catch (bad_alloc& e) {
		cout << "内存分配失败!!" << endl;
		cout << e.what() << endl; // 输出异常信息
	} // 捕获异常
	head->data = 0;
	head->next = NULL;
	return;
}

这里还有一点值得一提:
当链表内存分配失败时,我是用的是try…catch捕获的异常,这个使用于现在的大部分新版的编译器,因为新版编译器在内存分配失败的情况下将不再会返回NULL;老版的编译器入VC++6.0等就会返回NULL,所以一定要注意对内存分配失败的处理;

接下来就是创建一个单链表了,这里使用的是尾插法;
为什么使用尾插法呢?因为头插法输出的链表和输入的顺序是相反的,所以最好使用尾插法来创建链表;

这里创建的链表也需要注意:
该链表创建时会有一个没有什么实际意义的头节点,它的数据域存放的是链表的长度,当然创建头节点的目的也是为了方便对链表的操作;

代码如下:

void creatList(NODE*& head) {
	// 1,确定创建链表长度
	int len;
	cout << "请输入创建链表长度:" << endl;
	cin >> len;
	// 链表长度不应该为0和负数
	if (len <= 0) {
		cout << "创建链表长度不能是0或负数" << endl;
		return;
	}
	head->data = len; // 头节点数据域存放链表长度

	// 2,创建一个尾节点
	NODE* tail = head; // 定义一个节点为尾节点,指向头节点,它将代替头节点移动
	tail->next = NULL;

	// 3,循环创建新的节点
	for (int i = 0; i < len; ++i) {
		cout << "请输入第" << i + 1 << "个数据" << endl;
		int val;
		cin >> val;
		NODE* newNode = NULL;// 创建一个新节点作为临时节点
		try {
			newNode = new NODE; 
		}
		catch (bad_alloc& e) {
			cout << "内存分配失败!!" << endl;
			cout << e.what() << endl;
		} // 捕获异常
		newNode->data = val; // 新节点数据域赋值
		tail->next = newNode; // 将新节点挂在尾节点后面
		newNode->next = NULL; // 新节点指针域为空
		tail = newNode; // 尾节点为新的节点
	}
	return;
}

这一步的操作一定要学会,因为只有创建出来链表后你才能对链表进行其他操作;

遍历输出链表和链表长度

下面的操作是遍历链表并输出和返回链表的长度;

为什么把这两个函数放一起?因为它们的操作可以说是一模一样,只是有很小的改动;

当然对链表遍历也是非常简单,所以不需要有太大的心理负担;


遍历输出链表代码如下:

void traverseList(NODE* head) {
	NODE* p = head->next; // 临时节点p指向头节点的下一个节点
	while (p) {
		cout << p->data << " ";
		p = p->next; // p移向下一个节点
	}
	cout << endl;
	return;
}

虽然简单,但是还是需要注意一点:
临时节点不要忘记,因为头节点是没有什么实际意义的节点,所以输出头节点并没有什么意义;

获取链表长度代码如下:

int listLength(NODE* head) {
	NODE* p = head->next; // 临时节点p指向头节点的下一个节点
	int len = 0; // 链表长度
	while (p) {
		++len; // 长度加一
		p = p->next; // p移向下一个节点
	}
	return len;
}

是不是很简单,只是只需要修改关键的一句代码即可求得链表长度;

排序操作

有时我们可能会遇到一些情况需要对链表进行排序,链表是线性存储结构,那么该如何排序呢?
其实也很简单,只需要将数据域的内容进行交换排序即可,也就是只对数据域进行操作,并不改变链表结构;

这里是顺序排序;
代码如下:

void sortList(NODE*& head) {
	int t;
	NODE* p;
	NODE* q;
	for (p = head->next; p != NULL; p = p->next) {
		for (q = p->next; q != NULL; q = q->next) {
			// 交换数据域的内容
			if (p->data > q->data) {
				t = p->data;
				p->data = q->data;
				q->data = t;
			}
		}
	}
}

这个排序算法并不是最优的,但是非常好理解,所以先学会一种方法再去突破吧!

插入操作和删除操作

单链表和数组都是线性存储结构,数组的优点是:可以实现快速查询;链表的的优点是:可以快速的实现插入和删除操作;
所以,插入和删除操作在单链表中是非常非常非常重要的!!

对于链表的插入和删除操作,只需要记住一点:插入/删除哪个位置,一定要找到该位置的前一个位置;

还是画个图吧:
在这里插入图片描述
我来描述一下这张图:

  • 想要在data3的位置插入新节点节点s,首先需要找到data3的前一个位置:data2的位置,也就是p指针指向的位置;
  • 接下来就是插入操作了
  • 第一步1:先把新节点s挂到data3上;(即让s节点指向data3节点)
  • 第二步2:断开p节点和data3节点的联系;
  • 第三步3:让p节点指向s节点;(即p节点指针域存放s节点地址)

下面就来看一下代码:

void insertListByPostion(NODE*& head, int data, int pos) {
	int i = 1;
	NODE* p = head;
	while (p && i < pos) {
		++i;
		p = p->next;
	}
	if (!p || i > pos) {
		cout << "插入位置不存在!!" << endl;
		return;
	}
	NODE* newNode = NULL;
	try {
		newNode = new NODE;
	}
	catch (bad_alloc& e) {
		cout << "内存分配失败!!" << endl;
		cout << e.what() << endl;
	} // 捕获异常
	newNode->data = data;
	newNode->next = p->next;
	p->next = newNode;
	head->data++;
	cout << "插入成功!!" << endl;
}

需要注意:插入节点位置必须存在,即首部尾部和中间,所以前面需要先判断插入位置是否存在;


删除操作更简单,如图:
在这里插入图片描述
同样描述一下该图:

  • 想要删除q节点,所以先找到q节点前一个位置:p节点;
  • 接下来是删除操作:
  • 第一步:让p节点指向r节点
  • 第二步:释放q节点内存空间

是不是很简单;

来看一下代码如何实现:

void deleteListByPostion(NODE*& head, int pos) {
	int i = 1;
	NODE* p = head;
	while (p->next && i < pos) {
		p = p->next;
		++i;
	}
	if (!p->next || i > pos) {
		cout << "删除位置不存在!!" << endl;
		return;
	}
	NODE* q = p->next;
	p->next = q->next;
	cout << "删除成功!!删除元素为:" << q->data << endl;
	delete q;
	head->data--;
}

同样需要注意:删除节点位置必须存在,所以需要先判断插入位置是否存在,因为删除只能删除节点只能删除存在的节点,所以判断条件和插入节点有些不同,不理解可以画个图细细品味;

按顺序合并两个有序链表

这个操作其实已经不算是单链表的基本操作了,它是将两个顺序的链表合并为一个顺序的链表,并且使用O(1)的空间复杂度,既不能使用额外空间,但是考研时经常会出现,所以就来实现一下;
其实力扣上有原题,可以看看我的这篇文章:21. 合并两个有序链表(C语言),这里就不再写解析了;

代码如下:

void mergeTwoLists(NODE*& l1, NODE*& l2, NODE*& list) {
	list->data = l1->data + l2->data; // 头节点数据域为两个链表个数之和
	NODE* p = list;
	NODE* list1 = l1->next; // list1为l1头节点下一个节点
	NODE* list2 = l2->next; // list2为l2头节点下一个节点
	// 要对没有头节点的链表进行操作,一定不能带上头节点进行比较
	while (list1 && list2) {
		if (list1->data < list2->data) {
			p->next = list1;
			list1 = list1->next;
		}
		else {
			p->next = list2;
			list2 = list2->next;
		}
		p = p->next;
	}
	p->next = list1 ? list1 : list2;
	return;
}

其实这里还是需要注意一点:
力扣上的测试链表是没有头指针的,所以我们的链表不能直接去使用力扣上的代码,需要进行一些小改动,但是基本的算法思想还是一样的;

逆序链表

这个操作是将链表逆置,且同样不能使用额外内存空间;这个力扣上也有原题,解析可以看看我的这篇文章:剑指 Offer 24. 反转链表(C语言)

这个操作其实非常简单,就是一个双指针操作;

代码如下:

void reverseList(NODE*& head, NODE*& list) {
	// 双指针
	list = head; // 先把头节点连接到新的链表上
	NODE* fast = head->next;
	NODE* slow = NULL;
	while (fast) {
		NODE* node = fast->next;
		fast->next = slow;
		slow = fast;
		fast = node;
	}
	list->next = slow;
}

同样需要注意:
力扣上的测试链表是没有头指针的,所以我们的链表不能直接去使用力扣上的代码,需要进行一些小改动,但是基本的算法思想还是一样的;

总结

链表的操作其实是非常多的,这里只是为你开一个头,不管你是刚学习数据结构的小白,还是有经验的老手,都希望这篇文章可以给你带来一些启发;
数据结构的学习并不一定会让你瞬间感觉到编程能力的提升,这也是很多人学完数据结构感觉没什么用的原因;但是正是数据结构描述了我们生活抽象的事物,让它们变成了代码展示出来,数据结构在你的编程路上是要一直学习和理解的,只有对底层有更深入的了解,你的编程之路才能走的更顺更远!!!
希望我们一起进步!!

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YXXYX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值