数据结构(链表)
题目1:反转链表
输入一个链表的头结点,反转该链表,并返回反转后链表的头结点。链表结点定义如下:
struct ListNode { int m_nKey; ListNode* m_pNext; };
分析:这是一道广为流传的微软面试题。由于这道题能够很好的反应出程序员思维是否严密,在微软之后已经有很多公司在面试时采用了这道题。
为了正确地反转一个链表,需要调整指针的指向。与指针操作相关代码总是容易出错的,因此最好在动手写程序之前作全面的分析。在面试的时候不急于动手而是一开始做仔细的分析和设计,将会给面试官留下很好的印象,因为在实际的软件开发中,设计的时间总是比写代码的时间长。与其很快地写出一段漏洞百出的代码,远不如用较多的时间写出一段健壮的代码。
为了将调整指针这个复杂的过程分析清楚,我们可以借助图形来直观地分析。假设下图中l、m和n是三个相邻的结点:
a?b?…?l mànà…
假设经过若干操作,我们已经把结点l之前的指针调整完毕,这些结点的m_pNext指针都指向前面一个结点。现在我们遍历到结点m。当然,我们需要把调整结点的m_pNext指针让它指向结点l。但注意一旦调整了指针的指向,链表就断开了,如下图所示:
a?b?…l?m nà…
因为已经没有指针指向结点n,我们没有办法再遍历到结点n了。因此为了避免链表断开,我们需要在调整m的m_pNext之前要把n保存下来。
接下来我们试着找到反转后链表的头结点。不难分析出反转后链表的头结点是原始链表的尾位结点。什么结点是尾结点?就是m_pNext为空指针的结点。
static Node<T> Reverse<T>(Node<T> node) { Node<T> current = null; Node<T> previous = null; Node<T> next = null; Node<T> reversedHeader = null; current = node; while (current != null) { next = current.Next; if (next == null) //如果next是null,则表示已经遍历到最后一个Node, { reversedHeader = current;//此时将需要返回的node指向current,不能等到遍历完了返回current,因为那时current必然为null } current.Next = previous; previous = current; current = next; } return reversedHeader; }
题目2:求链表的中间结点 如果链表中间结点总数为奇数,返回中间节点,如果结点总数为偶数,则返回中间两个结点的任意一个。
思路:定义两个指针,同时从链表的头结点出发,一个指针一次走一步,另一个指针一次走两步。当走的快的指针走到链表末尾时,走得慢的指针正好在链表中间。
static ChainNode<T> GetMiddleNode<T>(ChainNode<T> node) { if (node == null) { return null; } var currentNode = node; var skipNode = node; while (skipNode != null) { if (skipNode.Next == null || skipNode.Next.Next == null) { skipNode = null; } else { skipNode = skipNode.Next.Next; currentNode = currentNode.Next; } } return currentNode; }
题目3:判断一个单向链表是否形成了环形结构
思路:和前面的问题一样,定义两个指针,同时从链表的头结点出发,一个指针一次走一步,另一个指针一次走两步。如果走得快的指针追上走的慢的指针,那么链表就是环形链表,如果走得快的指针走到了链表的末尾(Next指向null)都没有追上第一个指针,那么链表就不是环形链表。
static bool IsCircle<T>(ChainNode<T> node) { if (node == null) { return false; } bool isCircle = false; var currentNode = node; var skipNode = node; while (skipNode != null && !isCircle) { if (skipNode.Next == null || skipNode.Next.Next == null) { skipNode = null; } else { skipNode = skipNode.Next.Next; currentNode = currentNode.Next; isCircle = Object.ReferenceEquals(skipNode, currentNode); } } return isCircle; }
题目4:合并两个排序的链表,输入两个递增排序的链表,合并这两个链表并使新链表中的结点任然是按照递增排序的。
思路:采用递归,考虑鲁棒性
static ChainNode<int> MergeNode(ChainNode<int> node1, ChainNode<int> node2) { if (node1 == null && node2 != null) { return node2;//鲁棒性考虑 } if (node2 == null && node1 != null) { return node1;//鲁棒性考虑 } if (node2 == null && node1 == null) { return null;//鲁棒性考虑 } ChainNode<int> mergedHead = null; if (node1.Value < node2.Value) { mergedHead = node1; mergedHead.Next = MergeNode(node1.Next, node2); } else { mergedHead = node2; mergedHead.Next = MergeNode(node1, node2.Next); } return mergedHead; }
题目5:从后往前打印链表
思路:采用递归
static void PrintReverseNode(ChainNode<int> node) { if (node != null) { PrintReverseNode(node.Next); Console.WriteLine(node.Value); } }
题目6:链表中环的入口结点
思路:
- 首先利用一快一慢两个指针,判断这个链表是否有环,并且当快指针套圈慢指针时,相交的结点必定在环内。(快的指针始终都沿着环打转)
- 从相交的结点开始向前Move,当再次回到该结点的时候,表示绕了一圈,即知道了该环有几个结点。
- 两个指针,一个指针从头开始移动,另一个从结点数开始移动,当两个指针相遇时即环的入口。
static ChainNode<T> GetEnterNode<T>(ChainNode<T> node) { var crossNode = GetCrossNode(node);//得到相交的Node if (crossNode == null) { return null; } int circleCount = 0; var circleNode = crossNode; circleCount++; while (!object.ReferenceEquals(circleNode.Next, crossNode))//从相交的结点开始绕环一圈 { circleCount++;//求出环的大小(结点数) circleNode = circleNode.Next; } var aheadNode = node;//定义一前一后两个结点 var behindNode = node; for (int i = 0; i < circleCount; i++) { aheadNode = aheadNode.Next;//根据环的大小,先移动前一个结点。 } while (!object.ReferenceEquals(aheadNode, behindNode)) //同时向前移动指针,直到相交。 { aheadNode = aheadNode.Next; behindNode = behindNode.Next; } return aheadNode; } static ChainNode<T> GetCrossNode<T>(ChainNode<T> node) { if (node == null) { return null; } ChainNode<T> crossNode = null; var currentNode = node; var skipNode = node; while (skipNode != null && crossNode == null) { if (skipNode.Next == null || skipNode.Next.Next == null) { skipNode = null; } else { skipNode = skipNode.Next.Next; currentNode = currentNode.Next; if (Object.ReferenceEquals(skipNode, currentNode)) { crossNode = currentNode; } } } return crossNode; }
题目7:删除链表中重复的结点。
题目:在一个排序的链表中,如何删除重复的结点?例如:1,2,3,3,4,4,5,删除重复结点后变成1,2,5
static ChainNode<int> DeleteDuplicateNodes(ChainNode<int> node) { if (node == null) { return null; //鲁棒性考虑 } ChainNode<int> current = node;//当前结点 ChainNode<int> previous = null;//前一个结点的指针 ChainNode<int> next = null;//下一个结点的指针 ChainNode<int> head = node;//最终要返回的头结点 while (current != null) { next = current.Next; if (next == null) { break; //鲁棒性考虑 如果已经到最后一个结点了,就打断循环。 } if (next.Value > current.Value)//如果后一个值比当前的值大 { previous = current;//记住当前的值,把它设置为之前的值。 current = current.Next;//移动当前指针。 } else//如果下个值和当前的值相等。(重复了) { while (next != null && next.Value == current.Value) { next = next.Next;//不停地向后移动指针,找出往后第一个不重复的值。 } if (previous == null)//如果之前的指针是空(头n个值就重复) { head = next;//将头结点指向第一个不重复的值 } else { previous.Next = next;//如果之前的指针不是空,将之前的指针指向next,即跳过一个重复的数字 } current = next;//将current的指针向后指 } } return head; }
题目8:求链表的倒数第k个结点
思路:定义两个指针,让前一个指针先走k-1步,然后两个指针一起走,当前一个指针走到最后一个结点的时候,第一个指针指向的就是倒数第k个结点
static Node<T> GetLastKNode<T>(Node<T> node, int k) { if (node == null || k <= 0) { return null;//鲁棒性考虑 } //定义一前一后两个指针 Node<T> fastNode = node; Node<T> behindNode = node; int n = 0; while (fastNode != null) { if (n == k - 1) { break; } fastNode = fastNode.Next; //让前一个指针先走k-1步 n = n + 1; } if (fastNode == null) { return null; //鲁棒性考虑,如果k比n大就返回null } //当前一个指针指向最后的时候,前一个指针就是倒数第k个结点。 while (fastNode.Next != null) { behindNode = behindNode.Next; fastNode = fastNode.Next; } return behindNode; }