链表与指针
链表是数据结构中重要一部分,因为链表中的节点是基础指针连接起来的,所以一些链表中的操作相对于顺序表来说较为便利,例如常见的插入,删除操作。除此之外,指针在链表中的巧妙运用还有许多,且看我与大家聊聊。
关于链表的基础知识我就不介绍了,下面来通过一些比较有意思,面试题中经常问的题目来体会下指针带给链表操作的便利。
链表节点的定义:
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
后面的节点。所以思路是我们可以将 p
和 p->next
的 val
值交换,然后删除节点 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->next
,pfast == 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 = head
,p2 == 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;
}
这次关于链表与指针的一些题目的巧妙思路就分享到这里!
心比天高,脚踏实地!