链表-快慢指针(C++)

一、链表

链表是由一组在内存中不必相连(不必相连:可以连续也可以不连续)的内存结构Node,按特定的顺序链接在一起的抽象数据类型。

我们常见的链表结构有单链表和双向链表。

单链表,保存了下一个结点的指针,可以根据下一结点指针向下遍历

双向链表,保存了向上一个结点和向下一个结点的指针,所以可以向上下两个方向遍历。

单链表结构一般如下:

template<typename T>
struct Node
{
	T data; //数据
	Node* next; //指向下一个结点的指针
};

双向链表的结构一般如下:

template<typename T>
struct Node
{
	T data; //数据
	Node* previous; //指向上一个结点的指针
	Node* next; //指向下一个结点的指针
};

在算法中,我们遇到的链表题目一般可以使用一些经典的思路求解。本篇博客主要讨论这些经典的链表解题思路之一,快慢指针。

二、快慢指针

快慢指针也称龟兔赛跑算法,又叫判圈算法。具体方法是声明两个指针fast 指针和slow 指针,这两个指针按照题目的需求有不同的行进方案(例如fast每次行进2步,slow每次行进1步),但基本上步长是不同的,所以叫龟兔赛跑算法。

比较经典的题目如下,判断链表是否有环,所以又叫判圈算法。

【题目】环形链表

判断一个单链表是否有环,并且返回第一个入环节点。

一个单链表有环,即如下形式,图中第一个入环节点为2

<算法简述>

使用快慢指针求解这个问题。我们使fast和slow同时从头结点出发,同频行进。

其中fast每次走2个结点,slow每次走一个结点。

如果fast或 slow能够为nullptr,则链表无环,如果fast和slow相遇,则链表有环。

这里很容易比较得出快慢指针的一个特点1:可以判圈

使slow指针在原地(相遇位置)保持不动,并使fast指针返回头节点处,二者同频出发,每次均走一步。则再次相遇的节点为第一个入环节点。

<C++代码实现>

ListNode* hasCycle(ListNode* head)
{
	ListNode* fast = head;
	ListNode* slow = head;

    //判断是否有环
	while (nullptr != fast && nullptr != slow)
	{
		if (nullptr == fast->next) return nullptr;

		fast = fast->next->next;
		slow = slow->next;

		//无环,返回空指针
		if (nullptr == fast || nullptr == slow)
		{
			return nullptr;
		}

		if (fast == slow)
		{
			break;
		}
	}

	//快指针回到头结点,快慢指针每次均走一步,相遇节点为第一个入环节点
	fast = head;
	while (nullptr != slow)
	{
		if (fast == slow)
		{
			break;
		}
		fast = fast->next;
		slow = slow->next;
	}

	return slow;
}

<复杂度分析>

时间复杂度为O(N),额外空间复杂度为O(1)。

【题目】判断一个单链表是否是回文结构

回文结构:正向遍历和反向遍历每个结点都是相同数据的结构。例如1->2->1,1->2->2->1都是回文结构。

【方法一】使用栈结构(对照组)

        想到判断回文结构 ,我们可以想到栈结构,因为栈是符合先入后出的数据结构,所以只要我们先遍历一遍链表,对数据进行压栈。再依次弹出栈顶,就会得到链表的反向输出。这样可以同时取到原链表的正向输出和反向输出,一一比较之后,就可以判断是否回文。

<C++代码实现>

bool isPalindrome(Node* hd)
{
	stack<int> st;
	Node* temp = hd;
	while (nullptr != temp)
	{
		st.push(temp->data);
		temp = temp->next;
	}
	temp = hd;
	while (nullptr != temp)
	{
		if (temp ->data != st.top())
		{
			return false;
		}
		temp = temp->next;
		st.pop();
	}

	return true;
}

<复杂度分析>

上述额外使用栈结构,所以额外空间复杂度为O(N),时间复杂度为O(N)。

【方法二】快慢指针方法

分析:这个题目一般想不到使用快慢指针方法,但是快慢指针具有一个和此题吻合的特点2:利用快慢指针的不同步数,可以找到链表的中点。如果此题找到中点,我们也可以镜像地比较中点左右的元素,得出是否是回文结构的结论。

而使用下列方法,可以使额外空间复杂度为O(1),这就是这个算法的优势。

bool isPalindrome(Node* hd)
{
	if (nullptr == hd)
	{
		return true;
	}
	Node* fast = hd->next;
	Node* slow = hd;

	//快慢指针同时遍历,快指针每次走2步,慢指针每次走1步
	//当快指针到终点的时候,慢指针正好处于中点位置
	while (nullptr != fast && nullptr != fast->next)
	{
		fast = fast->next->next;
		slow = slow->next;
	}

	//慢指针继续往下遍历,遍历的同时将指向反向
	Node *temp1 = slow; //temp1用于记录slow的前一个结点,用于逆序链表的后半部分
	Node *temp2 = slow->next; //temp1用于记录slow的后一个结点,放在链表断开后指针丢失
	Node *mid = slow;   //保存中点结点,便于后续使用
	while (nullptr != slow)
	{
		slow->next = temp1;
		temp1 = slow;
		slow = temp2;
		if (nullptr != slow)
		{
			temp2 = slow->next;
		}
	}

	//此时temp1为原链表终点结点,使temp2指向头结点,同时遍历temp1,temp2比较数值
	Node *ed = temp1; //保存终点结点,便于后续还原链表
	temp2 = hd;
	bool ret = true; //是否是回文结构
	while (temp1 != mid)
	{
		if (temp1->data != temp2->data)
		{
			ret = false;
			break;
		}
		temp1 = temp1->next;
		temp2 = temp2->next;
	}

	//从终点开始,还原原链表
	temp1 = ed->next;
	ed->next = nullptr;
	while (mid != ed)
	{
		temp2 = ed;
		ed = temp1;
		temp1 = ed->next;
		ed->next = temp2;
	}

	return ret;
}

<复杂度分析>

可见此算法的coding复杂很多,但是额外空间复杂度可以做到O(1),时间复杂度为O(N)。

【题目】删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

<分析>

此题的重点难点是:如何找到倒数第N个结点。

做完上面的判断回文结构的题,我们认识到快慢指针的特点2,应该可以很容易想到:我们使fast先走N个结点,再让slow和fast同频同步行进,当fast为最后一个结点的时候,slow所在结点即为倒数第N个结点。

<C++代码实现>

ListNode* removeNthFromEnd(ListNode* head, int n)
{
	ListNode* fast = head;
	ListNode* slow = head;

	//fast指针先走n步
	while (nullptr != fast && --n > 0)
	{
		fast = fast->next;
	}

	//如果链表长度小于n,则直接返回头指针
	if (nullptr == fast)
	{
		return head;
	}

	//如果倒数第n个就是头节点,则换头节点
	if (nullptr == fast->next)
	{
		head = head->next;
		delete slow;
		return head;
	}

	//fast和slow都前进,fast走到倒数第一个,则删除slow->next节点
	while (nullptr != fast->next->next)
	{
		slow = slow->next;
		fast = fast->next;
	}

	fast = slow->next;
	slow->next = slow->next->next;
	delete fast;

	return head;
}

三、总结

从上面的题目中,总结下快慢指针的一般特点

1、可以判断链表是否有环,并得出入环节点;

2、可以定位链表中的某个指定位置的结点;

后续在求解问题的过程中,如果还发现其他妙用,博主会持续更新。

约瑟夫问题是一个经典的算法问题,通常涉及到在一个循环链表中,从头开始按顺序计数,每到某个指定数字k(通常是除了链表长度之外的最大公约数),就删除当前节点并继续计数。这个过程会一直持续直到链表中只剩下一个节点为止。 在C++中,可以这样实现一个循环链表并解决约瑟夫问题: ```cpp #include <vector> using namespace std; struct Node { int data; Node* next; }; Node* createCycleList(int n, int k) { // 创建一个n节点的循环链表 vector<Node*> nodes(n); for (int i = 0; i < n; ++i) { nodes[i] = new Node{i}; if (i != 0) nodes[i - 1]->next = &nodes[i]; else nodes[n - 1]->next = nodes[0]; // 创建循环 } return nodes[0]; } void josephusProblem(Node* head, int k) { if (!head || !head->next) return; // 空链表或只有一个节点 Node* slow = head; Node* fast = head; // 找到链表的长度,因为快指针每次移动k步,慢指针每次一步 while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } // 当快指针到达链表的最后一个节点时,慢指针正好指向删除节点后的第一个节点 slow = head; for (int i = 1; i <= k - 1; ++i) slow = slow->next; // 删除节点并返回新头结点 delete slow; return josephusProblem(head->next, k); // 递归删除并更新头节点 } int main() { int n, k; cin >> n >> k; Node* listHead = createCycleList(n, k); cout << "Josephus Problem solution: "; josephusProblem(listHead, k); return 0; } ``` 在这个代码中,首先创建了一个循环链表,然后通过快慢指针找到起始删除位置,接着按照约瑟夫环的规则逐次删除节点,直至剩余一个节点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值