列举一些数组常见的处理手段,供发散思维,寻找解题切入点:
map<int, int> count
map<int, int> index
一些数组操作技巧:
链表相关的解题技巧与示例应用。
// 单向链表 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) {} }; // 双向链表 struct Node { int val; Node *prev; Node *next; };
链表需要一个链表头用于遍历整个链表,实际使用时,可以直接将链表的第一个结点作为链表头,也可以为链表单独设置一个链表头结点,这个头结点不存储数据,数据从头结点的下一个结点开始存储。
带独立头结点的链表在插入和删除时操作相对简单,因为不需要考虑插入前或删除后链表为空的情况。
在插入或删除链表节点时,如果链表没有单独的头结点,则需要处理边界条件,也就是插入前链表为空,或是删除的是头结点的情况,为了简化问题,可以在头部加入一个哨兵节点,以简化操作,如下:
链表插入:
// 在头结点为head的链表结尾插入新结点val ListNode* append(ListNode *head, int val) { ListNode dummy; dummy.next = head; ListNode *node = &dummy; while(node->next != nullptr) { node = node->next; } node->next = new ListNode(val); return dummy.next; }
链表删除:
// 删除链表中值为val的结点 ListNode *remove(ListNode *head, int val) { ListNode dummy; dummy.next = head; ListNode *node = &dummy; while(node->next != nullptr) { if(node->next->val == val) { node->next = node->next->next; break; } node = node->next; } return dummy.next; }
使用快慢指针可以轻松解决以下这类的问题:
对于定位链表中间结点,我们可设置两个指针p1和p2,初始时都指向链表头,然后p1一次走一步,p2一次走两步,当p2到达链表尾部时,p1所指的位置必然是链表的中间结点:
ListNode *middleNode(ListNode *head) { ListNode *slow = head; ListNode *fast = head; while(fast && fast->next) { slow = slow->next; fast = fast->next->next; } return slow; }
对于定位到链表的倒数第k个结点,同样是使用快慢指针p1和p2,让p2先走k步,再p1、p2一起走,这样,当p2到达链表尾部时,p1指向的就是链表的倒数第k个结点:
ListNode* getKthFromEnd(ListNode* head, int k) { ListNode *slow = head; ListNode *fast = head; while(k--) { fast = fast->next; } while(fast) { slow = slow->next; fast = fast->next; } return slow; }
对于寻找链表中环的入口节点,
O(1)时间删除单链表指定结点:
node->val = node->next->val; node->next = node->next->next;
缺陷:只能应用于除尾结点之外的结点,如果删除的是尾结点,则只能从头遍历。