[关闭]
@smilence 2016-05-05T08:02:38.000000Z 字数 7138 阅读 2138

码农求职套路谈 第三讲 script

youtube课程讲义


面试官访谈

A君 ( 欧洲人,senior engineer,general)

T君 (印度人, senior engineer, general)

T君 (华人,senior engineer, general)

杂项

ADT与Data Structure的区别:
ADT代表的是有某种功能,就像是个interface;而Data Structure是一种它的具体implementation。
e.g. Hashtable也可以用BST,只要实现lookup这种功能。用程序语言表示是两个concrete的class都implement了 ICanLookup 这个interface

Linked List 是 ADT还是Data Structure?
ADT 与具体实现以至于performance是无关的
List是ADT,无论是ArrayList还是LinkedList,都可以插入删除查找。
LinkedList插入删除很高效,但是有额外的overhead,根据index随机访问很低效。

说这些,用意在于思考:我为什么要选择这个Data Structure? 实际上首先是根据问题的特征选择ADT (a sequence of numbers),其次是根据performance的requirement来选择 (比如是一个数据流,并且需要插入删除)。
e.g. LRU - 需要插入删除的数据串,并且需要快速地检索

Stack/Queue - ADT 最简单的判定方法是他们有不同的implementation
Heap 堆栈 - Data Structure, Priority Queue 是 ADT
Queue 就是priority queue的一个特例

interface IQueue extends IPriorityQueue (where getPriority() returns getInsertedOrder());
class Heap implements IPriorityQueue;

JAVA的Stack是stack的一个实现,就像C++的PriorityQueue一样。用意在于你不需要去关心内在的实现。

第三讲 Linked List

  1. //Definition for singly-linked list.
  2. struct ListNode {
  3. int val;
  4. ListNode *next;
  5. ListNode(int x) : val(x), next(NULL) {}
  6. };

2.linked list的基本操作
对节点操作,无非就是修改next指针,如果是双向链表,还有prev指针。也决定了节点之间的相对位置,基本就是决定了这个linked list。

所有操作务必注意边界条件( curr == headcurr == tailcurr == NULL)
凡是修改list的操作,只需考虑 1. 谁的next指针会受到影响,则需要修正 (毕竟绝大多数的问题,next指针就是node唯一重要的member variable) 2.删除trash指针

  1. void delNode(ListNode *prev){
  2. ListNode *curr = prev->next;
  3. prev->next = prev->next->next;
  4. delete curr;
  5. }

3.Dummy Node

对于涉及head节点的操作,不如ListNode *dummy = new ListNode(0); dummy->next = head;;使针对head节点的处理与其他节点一致。最后返回dummy->next作为头指针即可。(dummy作为绝对参照物)

e.g. Given a linked list and a value x, partition it such that all nodes less than x come before nodes greater than or equal to x.

4.Runner technique
对于寻找list某个特定位置的问题,不妨:ListNode *chaser = head, *runner = head; chaserrunner以不同速率遍历,runner到达tail时,返回chaser。(这个办法实际上是解决单向链表无法随机访问,所以只能用walk through来确定相对位置, 毕竟绝对位置用计数就可以了)

e.g.1 Find the middle point of the linked list

  1. ListNode *midpoint( ListNode *head){
  2. ListNode *chaser = head, *runner = head;
  3. while( runner->next && runner->next->next ){
  4. chaser = chaser->next;
  5. runner = runner->next->next;
  6. }
  7. return chaser;
  8. }

e.g.2 Find the kth to last element of a singly linked list.
e.g.3 Given a circular linked list, return the node at the beginning of the loop.
e.g.4 Given a list, rotate the list to the right by k places, where k is non-negative. (Leetcode: Rotate List) (拆分成两个问题:找到拆分点,拆分)

模式识别

1.在遍历Linked list时,注意每次循环内只对一个或一对节点进行题意中的操作。

e.g.Reverse the linked list and return the new head.

  1. ListNode *reverse( ListNode *head ){
  2. //Of course, it can also be done by recursion/stack
  3. ListNode *prev = NULL;
  4. ListNode *next = head->next;
  5. while(head){
  6. head->next = prev;
  7. prev = head;
  8. head = next;
  9. next = next->next;
  10. }
  11. return prev;
  12. }

2.Swap Node 问题
根据Rule No.2, 交换两个节点,不存在删除,两个节点的prev节点,以及两个节点的next指针,分别受到影响。
因此可以 a. 先交换两个prev节点的next指针的值 ; b. 再交换这两个节点的next指针。
e.g. 1->2->3->4 => 3->4->2->1
无论这两个节点的相对位置和绝对位置,以上处理总是成立。只是可能可以进一步缩写代码。

e.g. Given a linked list, swap every two adjacent nodes and return its head.( Leetcode: Swap Nodes in Pairs)
Given 1->2->3->4, you should return the list as 2->1->4->3.每个节点的next指针都会发生变化。

  1. ListNode *swapPairs(ListNode *head) {
  2. if(!head) return head; // 注意边界条件
  3. // swap之后head可能发生变化
  4. ListNode *dummy = new ListNode(0);
  5. dummy->next = head;
  6. // 初始化
  7. ListNode *prev1 = dummy;
  8. ListNode *node1 = head, *node2 = head->next;
  9. // 一次循环只处理这一个pair
  10. while(node1 && node1->next){
  11. node2 = node1->next;
  12. // 这时候只需要考虑 (prev1, node1) 和 (node1 , node2) 相互独立的两对
  13. // swap the "next" of prev nodes;
  14. ListNode *tmp = prev1->next; // =node1
  15. prev1->next = node1->next; // = node2
  16. node1->next = tmp; // = node1
  17. //swap the "next" of current nodes;
  18. tmp = node1->next; // = node1
  19. node1->next = node2->next;
  20. node2->next = tmp; // = node1
  21. // 也可以写 prev2, node2, 然后再node1 = prev2 代入解方程
  22. // 可以根据 comment来缩写
  23. prev1 = node1;
  24. node1 = prev1->next; // 这里不写node1->next, 因为不想纠缠这里面的关系,从新的prev1出发即可
  25. }
  26. return dummy->next;
  27. }

3.处理两个长度未必相等的linked list的问题,循环的条件一般为 while( l1 && l2 ) ,再处理剩下非NULL 的list。 (而不是 while( l1 || l2 )

e.g Merge two sorted linked lists and return it as a new list.(Leetcode: Merge Two Sorted Lists)

4.如果对靠前节点的处理必须在靠后节点之后,即倒序访问问题,则可视为recursion问题,或可以用stack等效解决。(前面节点的解依赖于后面节点的解)

e.g.1 Traverse the linked list reversely.

  1. void traverse(ListNode *head){
  2. // 处理边界条件
  3. if( head == NULL )
  4. return;
  5. // Linked List 特别的地方
  6. traverse(head->next);
  7. visit(head);
  8. }

e.g.2 Given input ( 7->1->6 ) + ( 5->9->2 ), output 2->1->9. // 无需递归,因为靠前节点不依赖于靠后节点
Follow up, Given input ( 6->1->7 ) + ( 2->9->5 ), output 9->1->2. // 用递归或stack处理,因为靠前节点的结果依赖靠后节点的处理, f(n) = Q( V(n), f(n-1) ), 这里的f(n)是进位 + linkedList的一个bundle (转换成string,先reverse再相加的空间复杂度是相同的)


e.g. A linked list is given such that each node contains an additional random pointer which could point to any node in the list or null.
Return a deep copy of the list.

  1. Node {
  2. T value,
  3. Node *next,
  4. Node *random,
  5. }

Deep Copy: 建立一个hashtable来记录从oldNode到newNode的mapping: table[oldNode] = newNode, 然后去遍历老的数据结构(既然是copy,至少是O(n)),建立并且利用这个mapping;

  1. RandomListNode *copyRandomList(RandomListNode *head) {
  2. unordered_map< RandomListNode *, RandomListNode *> table;
  3. RandomListNode *old = head;
  4. RandomListNode *dummy = new RandomListNode(0);
  5. RandomListNode *new = dummy;
  6. while(old){
  7. new->next = new RandomListNode(old->label);
  8. table[old] = new->next; // old->next = new->next
  9. old = old->next;
  10. new = new->next;
  11. }
  12. new = dummy->next;
  13. old = head;
  14. while(old){
  15. if(old->random)
  16. new->random = table[old->random];
  17. old = old->next;
  18. new = new->next;
  19. }
  20. return dummy->next;
  21. }

这个问题一定要两次遍历,因为既可能依赖于前面的又可能依赖于后面的计算结果:反过来想,两次遍历是一个解决这类问题的一个思路

Shallow copy verus Deep copy (给所有的member variable也建立一份copy)
cloneGraph 也是类似的, 建立mapping的同时,利用这个mapping,类似于DP的思想

  1. GraphNode *cloneGraph(GraphNode *node, unordered_map<GraphNode *,GraphNode *>& table){
  2. if(!node) return NULL;
  3. if(table.count(node)>0) return table[node];
  4. GraphNode *newNode = new GraphNode(node->label);
  5. table[node] = newNode;
  6. for(auto it = (node->neighbors).begin(); it != (node->neighbors).end(); ++it)
  7. (newNode->neighbors).push_back(cloneGraph(*it,table));
  8. return newNode;
  9. }

考虑时间复杂度的时候要有全局概念,比如建立cache的空间复杂度是O(n),那么就不用去考虑递归的空间开销

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注