对于一个链表,C++ 的定义如下:
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) {} };
链表结构可由下表征:
力扣 203
移除链表元素
这个题由于涉及到头节点的变动,因此可以先设置一个空的前置节点 dummy,然后将 dummy 指向 head。为了对 dummy 的指针指向进行保留,这里复制一个指针 temp = dummy。接下来是两种情况,当 temp->next->val == val,那么 temp->next = temp->next->next。否则 temp 指针移动,temp = temp->next。
ListNode* removeElements (ListNode* head, int val) { struct ListNode * dummy = new ListNode (0 , head); struct ListNode * temp = dummy; while (temp->next != NULL ) { if (temp->next->val == val) { temp->next = temp->next->next; } else { temp = temp->next; } } return dummy->next; }
时间复杂度为 $\small {O (n)}$,空间复杂度为 $\small {O (1)}$。
力扣 707
设计链表
class MyLinkedList {public : MyLinkedList () { this ->head = new ListNode (0 ); this ->size = 0 ; } int get (int index) { if (index < 0 || index >= size) { return -1 ; } ListNode* cur = head; for (int i = 0 ; i <= index; i++) { cur = cur->next; } return cur->val; } void addAtHead (int val) { addAtIndex (0 , val); } void addAtTail (int val) { addAtIndex (size, val); } void addAtIndex (int index, int val) { if (index > size) return ; index = max (0 , index); size++; ListNode* temp = head; for (int i = 0 ; i < index; i++) { temp = temp->next; } ListNode* NewNode = new ListNode (val); NewNode->next = temp->next; temp->next = NewNode; } void deleteAtIndex (int index) { if (index < 0 || index >= size) return ; size--; ListNode* temp = head; for (int i = 0 ; i < index; i++) { temp = temp->next; } ListNode* p = temp->next; temp->next = temp->next->next; delete p; } private : 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) {} }; int size; ListNode* head; };
力扣 206
反转链表
这个题可以用双指针的解法。设定慢指针 slow = NULL,快指针 fast = head。现在开始遍历。设置 temp = fast->head,保存 fast 的下一个节点。这里为了反转链表,需要将 fast->next 指向上一个节点 slow,因此有 fast->next = slow。现在反转了链表。开始移动 fast 和 slow 指针。这里先移动 slow 指针,由于现在 slow 已经完成任务,slow 需要向右移动一位,也就是 slow = fast。然后 fast 也向右移动一位,也就是 fast = temp。这一轮的反转就完成了。
由于 fast 比 slow 快一格,而题目要求要返回反转后的头节点,所以 fast 要遍历到原链表的 NULL 处。
ListNode* reverseList (ListNode* head) { ListNode* slow = NULL ; ListNode* fast = head; while (fast != NULL ) { ListNode* temp = fast->next; fast->next = slow; slow = fast; fast = temp; } return slow; }
力扣 24
两两交换链表中的节点
这个题说了两两交换,那么只研究前两个节点之间的连接关系就行。为了让节点访问更方便,还是要在链表前插入一个虚拟头节点 dummy。那么对于 dummy 和 node1 和 node2,两两交换就是将节点 dummy->node1->node2 变成 dummy->node2->node1,其他两两节点可依此类推。设定节点 temp = dummy,想完成交换,首先让 temp -> node2,然后 node1 -> node3,最后 node2 -> node1。然后更新 temp 向右移动两个节点,也就是 temp = node1(此时 node1 已经成为了链表中第二个节点),开始下一轮循环。循环的终止条件需要注意,要把 temp->next != NULL 放在前面,不然如果 temp->next == NULL,temp->next->next 就是非法访问,会编译出错。
ListNode* swapPairs (ListNode* head) { ListNode* dummy = new ListNode (0 , head); ListNode* temp = dummy; while (temp->next != NULL && temp->next->next != NULL ) { ListNode* node1 = temp->next; ListNode* node2 = temp->next->next; temp->next = node2; node1->next = node2->next; node2->next = node1; temp = node1; } ListNode* res = dummy->next; delete dummy; return res; }
力扣 19
删除链表倒数第 N 个节点
这个题乍一看需要遍历两遍链表,第一遍遍历得到链表的大小,第二遍遍历得到需要删除的元素的位置。不过可以换个思路,将两个遍历合并成一个。这里需要用到双指针的策略。首先定义 fast 和 slow 指针,刚开始先移动 fast 指针,让 fast 和 slow 的距离为 n 个节点。然后开始同步移动 fast 和 slow,当 fast 的下一位是 NULL 时,此时 slow 的下一位就是要删除的节点,这个时候就只需要 slow->next = slow->next->next 即可删除节点。
ListNode* removeNthFromEnd (ListNode* head, int n) { ListNode* fast = new ListNode (0 , head); ListNode* slow = fast; ListNode* dummy = fast; if (n <= 0 ) { return dummy->next; } for (int count = 1 ; count <= n; count++) { if (fast->next != NULL ) { fast = fast->next; } else if (count < n) { return slow->next; } } while (fast->next != NULL ) { fast = fast->next; slow = slow->next; } ListNode* temp = slow->next; slow->next = slow->next->next; delete temp; return dummy->next; }
时间复杂度为 $\small {O (L)}$,$\small {L}$ 为链表长度。空间复杂度为 $\small {O (1)}$。
力扣 142
环形链表 Ⅱ
判断链表中有没有环,可以近似成操场跑圈的相遇问题。假设有两个人同时起跑,一个人快,一个人慢,那么经过一段时间,他们一定可以在操场上的某个位置相遇。那么对于这个题目,定义一个快指针 fast 和一个慢指针 slow ,慢指针每次移动一格,快指针每次移动两格,如果链表存在环,他们必定会在环中相遇,如果链表不存在环,他们不可能相遇。
解决了链表中是否有环的问题,接下来解决如何找到环的入口。定义进入环之前的路程为 $\small {a}$,slow 进入环后走了 $\small {b}$ 路程和 fast 相遇,环的路程为 $\small {b + c}$,那么可以得知在 slow 进入环前 fast 已经在环内行走了 $\small {n (b + c)}$ 的路程,其中有 $\small {n\geqslant 1,n\in \mathbf {N}_+}$。根据上面的条件,列出等式:
$$a+b=\frac{a+n\left( b+c \right) +b}{2}$$
化简后得:
$$a=\left( n-1 \right) \left( b+c \right) +c$$
这表明 slow 走过 $\small {n-1}$ 个完整的环后,再走过路程 $\small {c}$ 必定和路程 $\small {a}$ 相等。因此定义一个新的指针 index = head,index 和 slow 每次步进 1 个节点,那么相遇的节点就是环的入口。
ListNode *detectCycle (ListNode *head) { ListNode* fast = new ListNode (0 , head); ListNode* slow = fast; while (fast->next != NULL && fast->next->next != NULL ) { fast = fast->next->next; slow = slow->next; if (fast->next == slow->next) { ListNode* index = head; slow = slow->next; while (index != slow) { index = index->next; slow = slow->next; } return index; } } return nullptr ; }
力扣 15
三数之和
这个题用哈希表做会变得非常麻烦,还是得用双指针的方法。设定 i、left、right 分别对应三个数的元素下标,取值为:
int i = 0 ; i < nums.size ();int left = i + 1 ;int right = nums.size () - 1 ;
这里没有说返回的数组是有序的,那么可以先对数组从小到大进行排序,然后再遍历数组,这样做是因为三数相加为 0,也就是 $\small {a + b+c = 0}$,$\small {a\leqslant b\leqslant c}$。那么一定有 $\small {a\leqslant 0}$,因此有了第一个判断条件,如果 nums [i] > 0,直接返回结果,不用再遍历了。
然后对 $\small {a}$ 进行去重,为了说明去重的必要性,举例 [-1, -1, -1, 0, 1],可以看到结果是 [-1, 0, 1],但是 - 1 会有多个不同的取值。在这里就需要判断,如果当 i 元素和 i - 1 元素值相同,那么就跳过此轮遍历。
然后添加 while 循环,判断三数之和是否为 0,如果大于 0,说明 nums [right] 太大了,将 right- -,如果小于 0,说明 nums [left] 太小了,将 left++。如果三数之和等于 0,那么就保存 i、left、right 对应元素值。
这个时候我们还要注意,如果有这样的情况 [-1, -1, 0, 0, 1, 1],多个 left 和 right 满足要求,就需要对 left 和 right 进行去重。在等于 0 的判断下,添加 while 循环,当 right 和 right - 1 元素相等,right- -,当 left 和 left + 1 元素相等,left++。这里注意无论是否进入去重的 while 循环,最后都要更新 right 和 left。
所有 while 循环都要加上 right > left 条件防止出错。
vector<vector<int >> threeSum (vector<int >& nums) { vector<vector<int >> res; sort (nums.begin (), nums.end ()); for (int i = 0 ; i < nums.size (); i++) { if (nums[i] > 0 ) return res; if (i > 0 && nums[i] == nums[i - 1 ]) continue ; int left = i + 1 ; int right = nums.size () - 1 ; while (right > left) { if (nums[i] + nums[left] + nums[right] > 0 ) right--; else if (nums[i] + nums[left] + nums[right] < 0 ) left++; else { res.push_back (vector<int >{nums[i], nums[left], nums[right]}); while (right > left && nums[right] == nums[right - 1 ]) right--; while (right > left && nums[left] == nums[left + 1 ]) left++; right--; left++; } } } return res; }
时间复杂度是 $\small {O (N^2)}$。
力扣 18
四数之和
这个题目和三数之和原理一致,也是使用双指针的方法,不过由于这个题目是 4 数相加,而且 target 是任意值,在去重方面还是有些不一致。
vector<vector<int >> fourSum (vector<int >& nums, int target) { vector<vector<int >> res; sort (nums.begin (), nums.end ()); for (int k = 0 ; k < nums.size (); k++) { if (nums[k] > target && (nums[k] >= 0 || target >= 0 )) break ; if (k > 0 && nums[k] == nums[k - 1 ]) continue ; for (int i = k + 1 ; i < nums.size (); i++) { if (nums[k] + nums[i] > target && nums[i] >= 0 ) break ; if (nums[i] == nums[i - 1 ] && i > k + 1 ) continue ; int left = i + 1 ; int right = nums.size () - 1 ; while (right > left) { if ((long )nums[k] + nums[i] + nums[left] + nums[right] > target) right--; else if ((long )nums[k] + nums[i] + nums[left] + nums[right] < target) left++; else { res.push_back (vector<int >{nums[k], nums[i], nums[left], nums[right]}); while (right > left && nums[right] == nums[right - 1 ]) right--; while (right > left && nums[left] == nums[left + 1 ]) left++; right--; left++; } } } } return res; }