链表与指针

链表与指针

链表是数据结构中重要一部分,因为链表中的节点是基础指针连接起来的,所以一些链表中的操作相对于顺序表来说较为便利,例如常见的插入,删除操作。除此之外,指针在链表中的巧妙运用还有许多,且看我与大家聊聊。

关于链表的基础知识我就不介绍了,下面来通过一些比较有意思,面试题中经常问的题目来体会下指针带给链表操作的便利。

链表节点的定义:

typedef struct ListNode
{
	int val;
	ListNode* next;
	ListNode(int x) :val(x), next(nullptr) {}
}ListNode;

开胃菜~

题一:只给定单链表中某个结点p(并非最后一个结点,即p->next!=NULL)指针,删除该结点。

看到这个题目,我们首先想到的解法可能是遍历链表,遍历到这个要删除节点的前一个节点 ListNode* pre ,将pre的next指向下下个节点,即 pre->next = pre->next->next 。但如果不遍历,可以实现吗?

且看解法:

既然要求是删除一个节点,在不遍历的情况下,由已知信息,我们只能删除p后面的节点。所以思路是我们可以将 pp->nextval 值交换,然后删除节点 p->next

void Delete(ListNode* p)
{
    swap(p->val, p->next->val);
    ListNode* tmp = p->next;
    p->next = p->next->next;
    delete tmp;
}

题二:只给定单链表中某个结点p(非空结点),在p前面插入一个结点。

与题一思路相同,我们在不遍历的前提下,想要在 p 节点之前插入节点,也需要变通下思路,将节点插入 p 的后面,然后交换 val 值,就达到了我们将节点在 p 前插入的效果。

void Insert(ListNode* p, int value)
{
    int* newNode = new ListNode(value);
    newNode->next = p->next;
    p->next = newNode;
    swap(p->val, p->next->val);
}

来个热身题~

题三:给定两个单链表(headA,headB),检测两个链表是否有交点,如果有返回第一个交点。 m,n为链表A/B的长度

在这里插入图片描述

暴力法:时间复杂度o(mn)

  • 对链表A中的每一个结点 ai,遍历整个链表B并检查链表B中是否存在结点和 ai相同。

双指针法:时间复杂度o(m+n)

  • 创建两个指针 pA 和 pB,分别初始化为链表 A 和 B 的头结点。然后让它们向后逐结点遍历。
  • 当 pA 到达链表的尾部时,将它重定位到链表 B 的头结点;
  • 类似的,当 pB 到达链表的尾部时,将它重定位到链表 A 的头结点。
  • 若在某一时刻 pA 和 pB 相遇,则 pA/pB 为相交结点。

在这里插入图片描述

如图所示指针 pA 和 pB 走的路程相同,如果有交点 pA 和 pB 则一定会在交点 8 相遇。

ListNode* getIntersectionNode(ListNode* headA, ListNode* haedB)
{
    if(headA == nullptr || headB == nullptr) return nullptr;
    ListNode* head1 = headA;
    ListNode* head2 = headB;
    while(head1 != head2)
    {
        head1 = (head1 == nullptr ? headB : head1->next);
        head2 = (head2 == nullptr ? headA : head2->next);
    }
    return head1;
}

开始了~~

题四:给定单链表头结点,删除链表中倒数第n个结点。

两次遍历法:

  • 这个问题我们很容易解答,我们只要知道链表的长度L,就可以推出要删除的是正数第(L - n + 1)个节点。
  • 首先遍历一遍链表,算出长度L。
  • 然后再遍历一遍,删除第(L - n + 1)个节点。

当我们咔咔咔写完代码给面试官,面试官嘴角一动,微微笑,说:小伙子不错,这个题能一次遍历就完成吗?

这就要开动开动你的小脑筋啦,妙用指针~~

一次遍历法:

  • 设两个指针 p1 和 p2 。
  • 指针 p1 先走n步,接着 p1 和 p2 一起向后走,直到 p1 == nullptr
  • 此时 p2 指向了我们要删除的节点,将它删除。

你品品,是不是感觉自己看到这个方法,人都聪明了hhh~~

当然在具体操作时,p1 先走n+1步,方便我们删除节点。

在这里插入图片描述

ListNode* removeNthFromEnd(ListNode* head, int n)
{
    if(nullptr == head) return nullptr;

    ListNode* p1 = head;
    ListNode* p2 = head;
    while(p1 != nullptr)
    {
        if(n<0) p2 = p2->next;
        n--;
        p1 = p1->next;
    }
    if(n == 0) return head->next;//删除头节点
    p2->next = p2->next->next;
    return head;
}

正餐:链表与环~ ~ ~

题五:给定单链表,检测是否有环。

题六:给定单链表(head),如果有环的话请返回从头结点进入环的第一个节点。

题七:根据题六题意,求出链表的非环长度。

题八:根据题六题意,求出链表的环长度。

看到这些题,我们这篇关于链表与指针的讨论也就到了高潮,其实也说不上讨论,算是一些解题思路总结。


在这里插入图片描述

题五:

检测单链表是否有环,其实也就是我们遍历链表时是否会遇到已经遍历过的节点,我们可以将已经访问过的节点添加到哈希表中,当一个节点被重复添加时,我们就知道这个单链表有环。

这个是容易想到的一个思路,时间复杂度o(n),空间复杂度o(n)。

我们还可以想想看,两个速度不一样的人在操场跑步,会发生什么呢?速度快的那个人在经过一段时间后,会追上速度慢的那个人,也就是套圈了。

操场跑步与我们解答单链表是否有环有甚关系呢?

同样有个环呀~ ~ ~

我们就可以借鉴这个思路设立两个指针,一个跑的的快,一个跑的慢。

在环中,速度慢的指针会被追上,也就是指针相等了,这时我们就能断定这个单链表有环。

解题:

  • 设置 pslow 和 pfast 两个指针,pslow = head->nextpfast == head->next->next
  • pslow 每次走一步,pfast 每次走两步。(为什么步数非得是一步和两步呢?)
  • 进入循环,直到 pfast == pslow

在这里插入图片描述

//设置一个结构体存储环的信息
typedef struct message
{
	bool hasCycle;             //是否有环
	ListNode* equal;           //快指针追上慢指针的节点
	ListNode* IntoCycleNode;   //入环节点
	int L;                     //非环长度
	int R;                     //环长度
	message() :hasCycle(false), IntoCycleNode(nullptr), L(0), R(0) {}
}message;
void hasCycle(ListNode* head, message& m)
{
    	if (nullptr == head || nullptr == head->next) return;

	ListNode* slow = head->next;
	ListNode* fast = head->next->next;
	while (fast != slow)
	{
		if (nullptr == fast || nullptr == fast->next) return;
		slow = slow->next;
		fast = fast->next->next;
	}
	m.hasCycle = true;
    m.equal = fast;
}

题六:

想要用双指针的方法求得入环点,我们就需要仔细分析分析了。

先给出答案:

  • 设置 p1 和 p2 两个指针,p1 = headp2 == m.equal
  • 两个指针同时向后走,p1 == p2 时,当前节点为入环点。

为什么这个点就是入环点呢?为什么pslow 和 pfast 两个指针步数非得是一步和两步呢?

看我简单分析下:

在这里插入图片描述

  • 快指针比慢指针快两倍,当快指针追上慢指针后,快指针走的路程是慢指针的两倍。
  • 设非环长度L,环长度R,入环点到m.equal的距离X
  • 由上可得等式:2(L + X) = L + nR + X
  • 化简后:L = (n - 1)R + R - X

所以根据推算到等式 L = (n - 1)R + R - X 可知,pfast 走 R - X 步到入环点,从 head 开始走 L 步到入环点,也就是指针 p1 和 p2 走相同步数可到入环点。

void getNodeIntoCycle(ListNode* head, message& m)
{
    ListNode* p1 = head;
	ListNode* p2 = m.equal;
	while (p1 != p2)
	{
		p1 = p1->next;
		p2 = p2->next;
	}
	m.IntoCycleNode = p1;
}

题七:

题七,题八就容易多了

void getL(ListNode* head, message& m)
{
    ListNode* p = head;
	int count = 0;
	while (p != m.IntoCycleNode)
	{
		p = p->next;
		++count;
	}
	m.L = count;
}

题八:

void getR(ListNode* head, message& m)
{
    int count = 1;
	ListNode* p = m.IntoCycleNode->next;
	while (p != m.IntoCycleNode)
	{
		p = p->next;
		++count;
	}
	m.R = count;
}
//测试用例
int main()
{
	ListNode head(0);
	ListNode* p = &head;
	ListNode* p6 = nullptr;
	for (int i = 0; i<10; ++i)
	{
		ListNode* tmp = new ListNode(i+1);
		if (i + 1 == 6) p6 = tmp;
		p->next = tmp;
		p = tmp;
	}
	p->next = p6;

	message m;
	hasCycle(&head, m);
	getNodeIntoCycle(&head, m);
	getL(&head, m);
	getR(&head, m);
	cout << "是否有环:" << (m.hasCycle==true ? "是" : "否") << endl;
	cout << "入环节点:val = " << m.IntoCycleNode->val << endl;
	cout << "非环长度X:" << m.L << endl;
	cout << "环长度R:" << m.R << endl;

	//for (ListNode* q = &head; q != nullptr; q = q->next)
	//{
	//	cout << q->val << " ";
	//}
	//cout << endl;

	system("pause");
	return 0;
}

这次关于链表与指针的一些题目的巧妙思路就分享到这里!

心比天高,脚踏实地!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值