算法通关村第一关——链表青铜挑战笔记

1. 什么是单链表

单链表是一种常见的线性数据结构,由一系列结点组成。每个结点由包含数据元素的数据域和保存下一个结点地址的指针域构成。

算法通关村第一关——链表青铜挑战笔记_第1张图片
怎么理解?

举个例子吧,假如现在你要去找你的辅导员办件事,首先你去了辅导员办公室(第一个结点),办公室的另外一个老师告诉你,你要找的人不在这,他在食堂干饭;于是你去食堂,食堂的打饭阿姨告诉你,你要找的人不在这(已经吃完了),他在图书馆看书;于是你去了图书馆,你找到他了!

这个例子中办公室、食堂、图书馆都可以看作一个链表中的结点,每个结点都有人(指针域)告诉你下一个应该去哪里。这一串就是一个单链表。

算法通关村第一关——链表青铜挑战笔记_第2张图片

相比于数组,链表的大小可以根据需要动态调整(调整指针里保存的地址,例如在食堂的时候告诉你下一个是操场,你就会去操场而不是图书馆),并且插入和删除操作的时间复杂度较低(因为不需要大量移动数据)。

2. 链表的相关概念

  • 结点(Node):链表中的基本单位,包含数据元素和指向下一个结点的指针。
  • 头结点(Head Node):链表中的第一个结点,用于标识链表的起始位置。

3. 构建链表

直接上结论,构建链表的步骤如下:

  • 定义结点结构体:创建一个包含数据和指针的结构体,用于表示结点。
  • 创建头指针:创建一个指向链表第一个结点的指针,并初始化为空。
  • 添加结点:通过动态内存分配函数(如new)创建新结点,并将各个结点连接起来。

结点由数据域和指针域构成,这肯定是要我们自己定义数据类型了:

typedef struct link {
      int elem;//代表数据
      struct link* next;//代表指针
}Link;

注意: 这里使用typedef重命名类型名称为Link,而不必每次都写struct link,方便以后书写。

结点的指针指向下一个结点的地址,所以要和结点类型保持一致。就好像指向整型int的指针类型是int*,这里指向struct link类型结点的指针类型是 struct link*

现在就是要创建头指针、头结点,并让头指针指向头结点。因为以后学习链表创建链表肯定必不可少,所以封装一个创建链表的函数,它返回一个链表的头指针。这里创建一个0,1,2,3,4的链表

Link* initLink() {
	//创建头指针
	Link* head;
	//创建结点
	Link* node = new Link;
	node->elem = 0;
	node->next = NULL;
	//头指针指向第一个结点
	head = node;
	for (int i = 1; i <= 4; i++) {
		Link* newNode = new Link;
		newNode->elem = i;
		newNode->next = NULL;
		//让当前结点的指针指向下一个结点
		node->next = newNode;
		//更新node
		node = node->next;
	}
	//返回头指针
	return head;
}

注意:不能把Link* node = new Link;写成Link* node;

原因:因为 node 是一个指向 Link 类型结点的指针,逻辑上它应该指向一个实际的结点对象。而在没有为 node 分配内存空间时,试图访问 node 的成员变量 elem 和 next 会导致未定义行为。

解决办法:使用 new 关键字来动态分配内存,即动态创建一个 Link 类型的结点对象。

当执行到head = node;时,第一个结点相当于有两个指针指向,由于单链表只能一直向后访问,所以头结点是关键。以后的操作由node完成,head始终指向头结点。最后创建完如下

初始化链表

4. 链表的增删

增加结点: 在链表中增加结点可以分为三种情况:

  • 在链表的头部插入结点:将新结点的指针指向原来的头结点,并将头结点指针指向新结点。

算法通关村第一关——链表青铜挑战笔记_第3张图片

  • 在链表的中部插入结点:先找到要插入位置的前一个结点,将新结点的指针指向前一个结点的下一个结点,再将前一个结点的指针指向新结点

算法通关村第一关——链表青铜挑战笔记_第4张图片

  • 在链表的尾部插入结点:将新结点的指针指向空值(NULL),同时将最后一个结点的指针指向新结点。

算法通关村第一关——链表青铜挑战笔记_第5张图片
其中后面两种可以合成一种情况来写:

//在链表某一位置插入新结点
Link* insertNewNode(Link* head,Link* newNode, int position) {
	if (head == NULL) {
		return newNode;
	}
//getLinkLength()用于获取某一链表的结点数(长度)
	int size = getLinkLength(head);
	if (position < 1 || position > size + 1) {
		cout << "插入的位置错误" << endl;
		return head;
	}
	
	//插入为头结点
	if (position == 1) {
		head->next = newNode->next;//新结点的下一个是头结点
		head = newNode; //更新头指针的位置,指向新的第一个结点
		return head;
	}

	//其他位置
	Link* p = new Link;
	p = head;
	//找到插入位置的前驱结点
	for (int i = 1; i < position - 1; i++) {
		p = p->next;
	}
	newNode->next = p->next;
	p->next = newNode;
	return head;
}

删除结点: 在链表中删除结点也可以分为三种情况:

  • 删除链表的头结点:将头结点的指针指向下一个结点,并释放原头结点的内存。
  • 删除链表的中间结点:先找到要删除结点的前一个结点,将其指针指向要删除结点的下一个结点,再释放要删除结点的内存。
  • 删除链表的尾结点:遍历链表,找到倒数第二个结点,将其指针指向空值(NULL),并释放尾结点的内存。

最后两种也可以和为一种:

//删除链表中某一位置的结点
Link* deleteNode(Link* head, int position) {
	if (head == NULL) {
		return head;
	}

	int size = getLinkLength(head);
	if (position < 1 || position > size + 1) {
		cout << "删除的位置错误" << endl;
		return head;
	}

	if (position == 1) {
		head = head->next;
		return head;
	}else {
		//找到要删除结点的前驱结点
		Link* p = new Link;
		p = head;
		for (int i = 1; i < position - 1; i++) {
			p = p->next;
		}
		p->next = p->next->next;
		return head;
	}
}

5. 链表在不同位置增加或删除结点的问题及处理方法

当在链表的首部、中部或尾部增加或删除一个结点时,可能会遇到以下问题:

1. 在中部、尾部增加/删除结点:

  • 问题:需要改变头结点的指针指向新结点,可能导致丢失原来的头结点
  • 处理方法:在插入新结点之前,将头结点的指针保存到一个临时变量中,然后将新结点的指针指向这个临时变量。

例如上文增加一个结点时,我们会用Link* p = new Link;创建一个新的指针p来查找,这样就能保持头指针不变。

2. 在尾部增加/删除结点:

  • 问题:需要遍历整个链表才能找到倒数第二个结点,时间复杂度较高
  • 处理方法:在链表维护一个指向倒数第二个结点的指针,使得查找倒数第二个结点的时间复杂度为 O(1)。

你可能感兴趣的:(算法学习,算法,链表,笔记)