1、给定一个头结点为
head
的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。示例:
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
这个题可以按照链表的方式做,也可以结合双指针的方式进行解答。
(1)链表+数组
/**
* 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) {}
* };
*/
class Solution {
public:
ListNode* middleNode(ListNode* head) {
vector<ListNode*>A = {head}; //设定数组A,给数组A一个head
while (A.back()->next !=NULL){ //这里就要用到链表的指针,如果A指向的下一个为NULL则遍历完毕
A.push_back(A.back()->next);
}
return A[A.size()/2]; //最后在遍历完毕的A取后半部分得到结果
}
};
主要的思路就是让数组A接受链表的开头,然后利用指针遍历,最后取A的后半部分。
时间复杂度为O(n),空间复杂度O(n)(额外的数组A)。
(2)指针+链表
class Solution {
public:
ListNode* middleNode(ListNode* head) {
int i = 0;
ListNode*ans = head;
while(ans !=NULL){
i++;
ans = ans->next; //遍历链表获取总数i
}
int j = 0;
ans = head;
while (j < i/2){
j++;
ans = ans ->next; //让ans指针指到中间节点处,最后输出的就是ans以及之后的
}
return ans;
}
};
利用ans的指针指到中间节点之后输出,就可以得到后半部分的数组。
时间复杂度O(n),空间复杂度O(1),相对于数组+链表法有着进一步的优化。
(3)快慢指针法(这个方法真的好聪明!学到了!)
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode*fast = head; //fast每次走两个元素
ListNode*slow = head; //slow每次走一个元素,当fast遍历完毕的时候,slow刚好到中间节点
while ( fast != NULL & fast ->next !=NULL){ //条件要注意,不仅fast不能为NULL;fast ->next也不能为NULL
fast = fast ->next ->next;
slow = slow ->next;
}
return slow; //成功输出slow
}
};
时间复杂度O(n),空间复杂度O(1)。
这个方法虽然和指针+链表的方法时间复杂度和空间复杂度差距不大,但是这个方法使得代码更加简洁,可以设置更少的参数。这种思想十分值得学习。
2、给你一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点。示例:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
这个第一实际时间想到的就是用链表,因为要跳过一个节点,所以要设置两个链表,也就是下面的方法(1)。
(1)计算链表总长之后再找到需要跳过的节点
代码:
* 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) {}
* };
*/
class Solution {
public:
int length(ListNode*head){ //设置这个是为了保证head不改变的情况下获得总长度length,函数内部的数值改变不输出
int length = 0;
while(head->next != NULL){
length++;
head = head ->next;
}
return length;
}
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode*forward = new ListNode(0,head);
ListNode*ans = forward;
int i = length(head);
for (int j =0;j != i-n+1;++j){
ans = ans ->next;
}
ans ->next = ans->next->next; //前面的遍历,到了需要取消的节点的时候就跳过指针
ListNode*ans1 = forward -> next; //这里要注意是foward指向next,也就相当于forward最后一个结点指向了ans的开头;类似head ->next的效果
return ans1;
}
};
时间复杂度O(n),也就是链表的长度;空间复杂度O(1)。
(2)栈
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode*dummy = new ListNode(0,head);
ListNode*ans = dummy;
stack<ListNode*>stk;
while(ans){
stk.push(ans);
ans = ans ->next;
}
for (int i = 0;i<n;++i){
stk.pop(); //弹出删除结点以及之后的结点
}
ListNode*pre = stk.top(); //栈最顶上的就是删除结点的前一个
pre->next = pre->next->next;
ListNode*ans1 = dummy->next;
return ans1;
}
};
这里是利用栈push和pop的操作来找到需要删除的结点。简单易懂还方便。
空间复杂度O(n),时间复杂度O(n)。空间复杂度变大是因为用到了栈。
(3)双指针
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode*dummy = new ListNode(0,head);
ListNode*first = head;
ListNode*last = dummy; //这里为什么等于dummy没有太懂
for(int i = 0; i<n;++i){
first = first->next; //让first先走n步
}
while(first){
first = first->next;
last = last->next;
}
last ->next=last->next->next; //之后last就可以刚好到删除结点的前面
ListNode*ans = dummy->next;
return ans;
}
};
这里主要的思想是让first先走n步,之后last再追,当first走到尾部的时候last就可以刚好到删除结点的前面,就可以直接操作跳过删除结点了。这一种方法时间复杂度O(n),空间复杂度O(1)。开销虽然和(1)差不多,但是这个方法相对更加巧妙。