算法学习——链表

本文详细介绍了链表的基础知识,包括单链表、双链表和循环链表的定义,以及链表的常见操作如移除元素、反转链表、两两交换节点、删除倒数第N个节点和查找链表相交。还涉及到了链表相交和环形链表的检测方法,重点讲解了使用虚拟头结点、迭代和递归等技术。
摘要由CSDN通过智能技术生成

链表基础知识

链表的每一个节点由两部分组成,分为数据域和指针域(存放指向下一个节点的指针),最后一个节点的指针域指向NULL。
在这里插入图片描述

链表有以下几种类型:

1.单链表。如上图。
2.双链表。每个节点有两个指针域,一个指向上一个节点,一个指向下一个节点。
在这里插入图片描述
3.循环链表。也就是链表首尾相连,链表的尾节点的指针指向头节点。
在这里插入图片描述

链表的定义方式

struct ListNode
{
	int data;
	struct ListNode * next;
	ListNode(int val):data(val),next(NULL) //节点的构造函数
	{}
};

定义构造函数是为了方便我们创建节点的时候能够直接给节点初始化。
如果我们不定义构造函数,使用编译器默认生成的构造函数,ListNode* head = new ListNode(); head->val = 5;需要这两行代码。
而我们自己定义构造函数只需要:ListNode* head = new ListNode(5);

链表的操作

删除节点
在这里插入图片描述
添加节点
在这里插入图片描述

移除链表元素

力扣题目链接

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

在这里插入
图片描述
首先我们看一下力扣对链表的定义方式,之后代码要用该格式来写:

  Definition for singly-linked list.
 struct ListNode {
      int val;
      ListNode *next;
      ListNode() : val(0), next(nullptr) {}
      ListNode(int x) : val(x), next(nullptr) {}
      ListNode(int x, ListNode *next) : val(x), next(next) {}
  };
 

提供了三种不同的构造函数。

接下来,我们分析题目。在该链表中,头节点是有数据的。因此如果要删除第一个节点的话,我们需要另一个节点来存放这个头节点的下一个节点位置。
但是我们可以创建一个不存放数据的头节点,然后指向这个头节点,那么无论删除链表里的任何元素,都是一样的方式。

注意:在做第二遍的时候发现以下问题
1.在while循环中,有两个分支,一个是判断下一个节点的val与删除的val相等,另一个则是不等就指针后移。else是一定要有的,否则删除节点后指针还会后移,跳过了一个节点。
2.在if中一定要创建一个节点,将待删除节点赋值给该节点,然后delete释放,否则会出现如下输出为[7,7,7,7]的情况。
3.我们使用的是虚拟头节点方法进行删除,所以需要一个虚拟头节点,还有一个指向虚拟头节点的遍历指针。因为用虚拟头节点遍历的话,最后返回的就不是我们需要的头节点了。

在这里插入图片描述

不能为了省事直接return head,因为head有可能被我们删了,所以我们需要return 虚拟头节点的下一个节点。

代码如下:

        ListNode * virtualHead = new ListNode(0);
        virtualHead->next = head;
        ListNode *p = virtualHead;
        while (p->next != NULL)
		{
			if (p->next->val == val)
			{
				ListNode *q = p->next;
				p->next = q->next;
                delete q;
			}
			else
			{
				p = p->next;
			}
		}
        head = virtualHead->next;
        delete virtualHead;
        return head;

在这里插入图片描述

设计链表

力扣题目链接

你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

示例:
在这里插入图片描述
这道题中,我们要在一个链表类中实现链表的定义和操作。
因为是第一次在类中实现数据结构,我这里再写一遍。

class MyLinkedList
{
public:
		struct LinkedNode
		{
			int val;
			struct LinkedNode *next;
			//继续使用构造函数来给链表节点初始化
			LinkedNode(int a):val(a),next(NULL){}
		};
		//使用类的构造函数给链表初始化
		MyLinkedList()
		{
			size = 0;
			head = new LinkedNode(0);
		}
		int size;
		LinkedNode *head;
};

接下来的实现中,我只写一下我出错的代码。
void addAtIndex(int index, int val);
这是实现将val节点插入到下标为index的节点的前面的函数。此外,如果index等于链表长度,那么该节点会被插入到链表尾。
我们需要知道下标1是从0开始的,也就是说下标index==size的节点是不存在的,但是这个插入操作是插入到index节点之前的,所以也就是插入到链表尾了。

void addAtIndex(int index, int val)
	{
        LinkedNode *p = head;
		LinkedNode *q = new LinkedNode(val);
		if (index>size) return;
		if (index < 0) index = 0;
		while (index)
		{
			p = p->next;
			index--;
		}

		q->next = p->next;
		p->next = q;
		size++;
		return;
	}

反转链表

力扣题目链接

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
在这里插入图片描述
这道题目很简单,就是取下来节点然后头插法插入就行了。这里我直接给出代码。

class Solution {
public:
    ListNode* reverseList(ListNode* head) 
	{
		ListNode *virtualHead = new ListNode(0);
		while (head != NULL)
		{
			ListNode *p = new ListNode(head->val);
			p->next = virtualHead->next;
			virtualHead->next = p;
			head = head->next;
		}
		return virtualHead->next;
	}
};

在这里插入图片描述
但是,我看见双指针法以及递归法也能实现这个过程,这里我们学习一下这种进阶方法。
如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:

在这里插入图片描述
我们首先定义一个cur指针,指向头节点,代表的是当前节点。再定义一个pre指针,初始化为空,代表当前节点的前一个节点。
接着思考如何实现反转?
我们反转时候会将cur->next指向pre,但是这样做之后我们怎么继续遍历链表?
此时,就需要一个临时存储cur->next的变量,时刻保存cur的下一个节点位置。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* temp; // 保存cur的下一个节点
        ListNode* cur = head;
        ListNode* pre = NULL;
        while(cur) {
            temp = cur->next;  // 保存一下 cur的下一个节点,因为接下来要改变cur->next
            cur->next = pre; // 翻转操作
            // 更新pre 和 cur指针
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};

接下来就是递归法
其实,递归法的逻辑与双指针法一样,都是通过翻转指针指向来实现反转链表的。
如何理解呢?
首先,reverse函数有两个参数,一个是cur,一个是pre。其次,我们需要知道递归函数的初始化参数如何赋值。
在双指针方法中,cur初始化指向head,pre指向空。那么递归函数reverse(head,NULL)作为入口,接着进行递归。
递归函数进去首先要有一个判断跳出递归的条件,在我们双指针实现中是cur指向NULL,pre指向链表最后一个节点的时候就跳出循环了。这里也是如此,当cur指向NULL时,递归终止,我们需要返回pre的指针作为反转后的头节点。
在这里插入图片描述

class Solution {
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
        if(cur == NULL) return pre;
        ListNode* temp = cur->next;
        cur->next = pre;
        // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
        // 和双指针法初始化是一样的逻辑
        // ListNode* cur = head;
        // ListNode* pre = NULL;
        return reverse(NULL, head);
    }

};

两两交换链表中的节点

力扣题目链接

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
在这里插入图片描述
我的思路如下:
题目要求两两交换,那么我们先考虑两两交换是如何实现的?以上图前两个节点为例,我的做法是,创建三个节点,一个是头节点,next指向1,然后剩下两个节点分别存储头节点的next与next->next,假设一个是上图1,一个是上图2。我们可以发现,2节点的next对接下来遍历很重要,因为1这个节点和2换完位置后,1的next需要指向原来2的next的节点,所以我们需要先把2的next指针的值给1,然后再修改2的next为1,这样的顺序才可以保证节点顺序不丢失。
最后上述例子节点顺序就变成dummyhead->2->1->3->4。
然后我们思考一下,在什么位置开始交换两个点。
这个位置就是待交换两个点的前一个节点,然后这个节点的后一个节点以及后一个节点的后一个节点就是上述例子中的1、2两点,交换步骤一致。
最后,我们循环的终止条件呢?就是当前节点的next为空,说明当前节点是最后一个节点,不需要交换了。
注意:需要交换的两个点如何辨别?一个是设置一个count计数器来让我们隔开已经交换完的两个点,还有一个是遍历的指针的next->next不为空,说明之后有两个点,那么就可以交换。
在这里插入图片描述

在这里插入图片描述

删除链表的倒数第N个节点

力扣题目链接

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
在这里插入图片描述
这道题思路如下:
首先我们需要知道链表有多少个节点才能顺着next指针找到待删除节点。此外,我们找到的其实是待删除节点的上一个节点。
接下来就是删除条件的确定了,这个我们可以找特殊的例子来判断我们的条件是否正确。
在这里插入图片描述
假设我们已经知道了链表有多少个节点,值为count,且我们创建了一个头结点,指向链表的第一个节点。
在上面的例子中,我们删除的是第一个节点,但我们需要找到的节点是它前面的节点,也就是头结点,我们遍历的时候,头结点的序号是0,第一个节点是1,我们遍历count-n次就可以找到被删除节点的前一个节点了,在上述例子中就是头结点。
同时,在遍历条件中,count与i进行比较为什么是<而不是<=,同样以上述为例,如果有等于号,p指针会指向第一个节点,不是我们想要的结果。
在这里插入图片描述
在这里插入图片描述
该题还有另一种解法:双指针法
如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
在这里插入图片描述
在这里插入图片描述

链表相交

力扣题目链接

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
在这里插入图片描述
这道题的思路如下:
首先我们需要知道,两条链表如果有相交的节点,那么该节点之后的节点都是一样的。如果是两条一样长度的链表,我们可以同时遍历他们,直到找到相同的节点。但是两条不等的链表,如果我们同时开始遍历,是永远也找不到相同的节点的,我们需要让长的链表先遍历,然后再和短的链表同时遍历。
以上图为例:
B链表从b2开始与A链表一起遍历,才能找到相同的节点。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

环形链表II

力扣题目链接

题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
在这里插入图片描述
这道题分为两部分:
1.我们如何判断一个链表是否有环?2.链表有环的话,如何找到环的入口节点。

首先解决第一个问题。这个必须用到快慢指针,并且他们的步差是1个节点,保证快指针追赶慢指针的时候不会跳过慢指针。在这里,我们设定快指针一次走两个节点,慢指针一次走一个节点。那么在链表有环的情况下,无论如何快指针都会追上慢指针。
第二,如何找到入口。
在这里插入图片描述
在相遇这一时刻,slow指针走了x+y的节点数,fast指针走了x+y+n(y+z)。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2。
(x + y) * 2 = x + y + n (y + z)
化简可以得到:x + y = n (y + z)
我们想要知道的是x的大小,那么原式可以化简为:x = (n - 1) (y + z) + z
当n等于1时,那么x就等于z。
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。

代码如下:
在这里插入图片描述
在这里插入图片描述

总结

我们首先需要掌握链表的增删改查等基本操作。
其次,为了对链表进行方便的操作,我们一般使用虚拟头结点。
反转链表是高频题目,主要有迭代法和递归法。
在删除倒数N个节点的题目中,使用了双指针法。
最后的环形链表主要是数学证明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值