zoukankan      html  css  js  c++  java
  • 面试算法之链表操作集锦

    链表操作在面试过程中也是很重要的一部分,因为它和二叉树一样都涉及到大量指针的操作,而且链表本身很灵活,很考查编程功底,所以是很值得考的地方。下面是本文所要用到链表节点的定义:

    template <typename Type>
    struct ListNode{
        Type data;
        ListNode *next;
    };

    链表的创建可以采用下面的代码,采用尾插法进行链表的创建,返回的链表没有头节点:

    /**
     * Create a list, without head node
     */
    template <typename Type>
    ListNode<Type> *CreatList(Type *data, int len)
    {
    	if(data == NULL || len <= 0)
    		return NULL;
    
    	ListNode<Type> *head, *last;
    
    	head = new ListNode<Type>;
    	last = head;
    
    	for (int i = 0; i < len; ++i)
    	{
    		last->next = new ListNode<Type>;
    		last->next->data = data[i];
    		last = last->next;
    	}
    
    	last->next = NULL;
    
    	last = head;
    	head = head->next;
    
    	delete last;
    
    	return head;
    }

    1.单链表的逆序打印

    单链表的逆序打印就是重表尾开始依次往前打印,直到表头截止,所以可以将链表逆置,然后顺序打印,但这是一种劳民伤财的做法,不仅容易出错,而且还破坏了链表的结构。这里可以采用额外的空间,来保存顺序遍历的节点,在遍历完后,就可以将该辅助空间的值逆序输出,下面是采用stack实现的代码:

    /**
     * reversely print the list
     * method 1: use the stack
     */
    template <typename Type>
    void ReversePrintList_1(const ListNode<Type> *head)
    {
    	if(head == NULL)
    		return;
    
    	stack<Type> nodeStack;
    	while (head)
    	{
    		nodeStack.push(head->data);
    		head = head->next;
    	}
    
    	while(!nodeStack.empty())
    	{
    		cout<<nodeStack.top()<<" ";
    		nodeStack.pop();
    	}
    	cout<<endl;
    }
    

    我们知道代码中,递归和栈很多时候可以相互转化,而且通过递归实现的代码会更加简洁。下面是通过递归的方式,来实现上面的功能,代码如下:

    /**
     * reversely print the list
     * method 2: recursively
     */
    template <typename Type>
    void ReversePrintList_2(const ListNode<Type> *head)
    {
    	if(head == NULL)
    		return;
    
    	ReversePrintList_2(head->next);
    	cout<<head->data<<" ";
    }

    2.单链表的逆置

    在前面单链表的逆序打印中,有一种方法就是把单链表逆置后,再顺序打印。单链表的逆置最高效的方法,就是顺序扫描链表,然后依次逆置,代码如下:

    /**
     * reverse the list
     * method 1:sequential scanning 
     */
    template <typename Type>
    ListNode<Type> * ReverseList_1(ListNode<Type> *head)
    {
    	if(head == NULL)
    		return NULL;
    
    	ListNode<Type> *pre = NULL;
    
    	while (head)
    	{
    		ListNode<Type> *nextNode= head->next;
    		head->next = pre;
    		pre = head;
    		head = nextNode;
    	}
    
    	return pre;
    }

    同样可以通过递归的方式来进行逆置,递归的思想就是:将已经逆置的链表的最后一个节点返回,并把当前节点添到该节点的后面,单面如下:

    /**
     * reverse the list
     * method 2: recursion
     */
    template <typename Type>
    ListNode<Type> * ReverseList_2(ListNode<Type> *head)
    {
    	if(head == NULL)
    		return NULL;
    
    	ListNode<Type> *newHead;
    	SubReverseList_2(head, newHead);
    
    	return newHead;
    }
    
    template <typename Type>
    ListNode<Type> * SubReverseList_2(ListNode<Type> *head, ListNode<Type> *&newHead)
    {
    	if (head->next == NULL)
    	{
    		newHead = head;
    		return head;
    	}
    
    	ListNode<Type> *post = SubReverseList_2(head->next, newHead);
    	post->next = head;
    	head->next = NULL;
    
    	return head;
    }

    3.在O(1)时间删除链表节点

    题目是:在一个单链表中,通过节点的指针,在O(1)时间删除该节点。该题是一个投机取巧的方法,就是将要删除的节点用下一个节点覆盖,然后删除下一个节点就可以了。但如果该节点时尾节点,O(1)的时间是不成立的。代码如下:

    /**
     * delete a node from list
     */
    template <typename Type>
    ListNode<Type> * DeleteNode(ListNode<Type> *head, ListNode<Type> *node)
    {
    	if(head == NULL || node == NULL)
    		return head;
    
    	//only have one node
    	if (node == head && node->next == NULL)
    	{
    		delete head;
    		return NULL;
    	}
    
    	//node counts > 1, and delete the tail node
    	if (node->next == NULL)
    	{
    		ListNode<Type> *pre = head;
    
    		while (pre->next != node)
    			pre = pre->next;
    
    		delete node;
    		pre->next = NULL;
    
    		return head;
    	}
    
    	//other node
    	ListNode<Type> *delNode = node->next;
    	node->data = delNode->data;
    	node->next = delNode->next;
    
    	delete delNode;
    
    	return head;
    }

    4.链表中的倒数第k个节点

    求单链表的倒数第k个节点,其实是一个很简单的问题,最容易想到的是下面三种方法:

    • 遍历一遍节点统计链表的长度,然后计算出倒数第k个节点在链表中顺序的位置。
    • 可以通过stack来保存链表顺序扫描的节点,然后弹出第k个节点。
    • 可以通过递归来实现。

    但上面的方法都需要扫描链表超过一次或者是需要O(n)的辅助空间,如果要求只能扫描一遍链表,且是辅助空间为O(1),那么怎么解决呢。这里有一个很巧妙的方法:用两个指针p1,p2,初始都指向第一个节点,指针p1首先向后移动k-1个节点,然后两个指针一起向后移动,直到p1指向尾节点,那么p2所指向的就是倒数第k个节点,代码如下:

    /**
     * return the last k node from list, 1 =< k <= list length
     */
    template <typename Type>
    const ListNode<Type> * LastKNode(const ListNode<Type> *head, int k)
    {
    	if(head == NULL || k < 1)
    		return NULL;
    
    	const ListNode<Type> *ahead, *after;
    	after = ahead = head;
    
    	for (int i = 0; i < k - 1; ++i)
    	{
    		//the list length less than k
    		if(ahead->next == NULL)
    			return NULL;
    
    		ahead = ahead->next;
    	}
    
    	while (ahead->next != NULL)
    	{
    		ahead = ahead->next;
    		after = after->next;
    	}
    
    	return after;
    }

    5.合并两个排序的链表

    将两个排序的单链表合并成一个链表,方法很简单,这里首先创建一个头节点,将合并的链表链接在其后面,以简化代码。代码如下:

    /**
     * merge two sorted list
     */
    template <typename Type>
    ListNode<Type> * MergeTwoSortedList(ListNode<Type> *H1, ListNode<Type> *H2)
    {
        if (H1 == NULL)
            return H2;
        if (H2 == NULL)
            return H1;
    
        ListNode<Type> *head, *last;
    
        head = new ListNode<Type>;
        last = head;
    
        while (H1 != NULL && H2 != NULL)
        {
    		if (H1->data <= H2->data)
    		{
    			last->next = H1;
    			last = H1;
    			H1 = H1->next;
    		} 
    		else
    		{
    			last->next = H2;
    			last = H2;
    			H2 = H2->next;
    		}
        }
    
        if (H1 != NULL)
            last->next = H1;
        else if (H2 != NULL)
            last->next = H2;
    
        H1 = head->next;
        delete head;
    
        return H1;
    }
    

    6.求两个单链表的第一个公共结点

    如果两个单链表有公共结点,那么它们组成的形状一定是“Y”形的。求它们的第一个公共结点,可以有几种解法。

    • 最暴力的解法就是从一个链表的开头,把每个结点依次与另一个链表的所有结点进行依次比较,直到找到第一个公共结点为止,这种解法那叫一个暴力,时间复杂度为O(n^2),面试官肯定会鄙视的。
    • 采用stack辅助空间,分别将两个链表的各个结点依次入两个栈中,然后从两个栈中弹出结点,直到结点的内容不同为止,上一个结点就是所求。这种做法需要O(n)的辅助空间,估计也不是面试官最想要的。
    • 采用对齐的方法。计算两个链表的长度l1,l2,分别用两个指针p1,p2指向两个链表的头,然后将较长链表的p1(假设为p1)向后移动l2 - l1个结点,然后再同时向后移动p1,p2,直到p1 = p2。这种方法才是面试官最想要的,具体代码实现如下:
    /**
     * Find the first common node
     */
    template <typename Type>
    ListNode<Type> * Find1stCommonNode(ListNode<Type> *h1, ListNode<Type> *h2)
    {
    	if(h1 == NULL || h2 == NULL)
    		return NULL;
    
    	int len1, len2;
    
    	len1 = GetListLength(h1);
    	len2 = GetListLength(h2);
    
    	if (len1 > len2)
    	{
    		for (int i = 0;i < len1 - len2; ++i)
    			h1 = h1->next;
    	}
    	else
    	{
    		for (int i = 0;i < len2 - len1; ++i)
    			h2 = h2->next;
    	}
    
    	while (h1 && h1 != h2)
    	{
    		h1 = h1->next;
    		h2 = h2->next;
    	}
    
    	return h1;
    }
    
    template <typename Type>
    int GetListLength(const ListNode<Type> *head)
    {
    	int num = 0;
    
    	while (head)
    	{
    		++num;
    		head = head->next;
    	}
    
    	return num;
    }

    7.判断两个单链表是否相交

    由上面6可知,相交的单链表一定是“Y”形的,所以如果相交,那么最后的一个节点一定相同。所以很简单,代码如下:

    /**
     * judge two list crossing or not
     */
    template <typename Type>
    bool IsCrossing(ListNode<Type> *h1, ListNode<Type> *h2)
    {
    	if(h1 == NULL || h2 == NULL)
    		return false;
    
    	while(h1->next != NULL)
    		h1 = h1->next;
    	while(h2->next != NULL)
    		h2 = h2->next;
    
    	if(h1 == h2)
    		return true;
    	return false;
    }

    8.判断单链表是否存在环

    判断单链表是否存在环的思想就是判断遍历的结点是否已经遍历过。那么实现上最简单的就是通过辅助空间来保存已经遍历过的结点,在每遍历一个结点时判断该结点是否已经在空间中,如果在就说明有环,否则把该结点写入辅助空间,直到找到环或访问链表结束。可以通过hashmap来保存访问的结点,查找效率是O(1)。但是需要O(n)的辅助空间。面试官想要的方法是:通过两个指针,分别从链表的头结点出发,一个每次向后移动1步,另一个移动两步,两个指针移动速度不一样,如果存在环,那么两个指针一定会在环里相遇。代码如下:

    /**
     * judge the list has circle or not
     */
    template <typename Type>
    bool HasCircle(ListNode<Type> *head)
    {
    	if(head == NULL)
    		return false;
    
    	ListNode<Type> *fast, *slow;
    	fast = slow = head;
    
    	while (fast && fast->next != NULL)
    	{
    		fast = fast->next->next;
    		slow = slow->next;
    
    		if(fast == slow)
    			return true;
    	}
    
    	return false;
    }

    9.求链表的中间结点

    题目:求链表的中间结点,如果链表的长度为偶数,返回中间两个结点的任意一个,若为奇数,则返回中间结点。

    此题的解决思路和第4题求链表的倒数第k个结点很相似。可以先求链表的长度,然后计算出中间结点所在链表顺序的位置。但是如果要求只能扫描一遍链表,如何解决呢?最高效额解法和第4题一样,通过两个指针来完成。用两个指针从链表头结点开始,一个指针每次向后移动两个结点,一个每次移动一个结点,直到移动快的那个指针移到到尾结点,那么慢的那个指针即是所求。代码如下:

    /**
     * get the middle node of list 
     */
    template <typename Type>
    const ListNode<Type> * ListMidNode(const ListNode<Type> *head)
    {
    	if(head == NULL)
    		return NULL;
    
    	const ListNode<Type> *fast, *slow;
    	fast = slow = head;
    
    	while(fast && fast->next != NULL)
    	{
    		fast = fast->next->next;
    		slow = slow->next;
    	}
    
    	return slow;
    }

    如果要求在链表长度为偶数的情况下,返回中间两个结点的第一个,那么代码中的while循环判断条件可以改为如下:

    while(fast && fast->next != NULL && fast->next->next != NULL)

    由题4,7,8可知道,在链表的问题中,通过两个的指针来提高效率是很值得考虑的一个解决方案,所以一定要记住这种解题思路,秒杀面试官吧。。。
    先写这么多,以后慢慢在加吧, 有新的问题大家可以提出来,一起讨论,共同进步...<^_^>。。。

    Date: Sept 4rd, 2013 @lab

  • 相关阅读:
    Spring Cloud 学习推荐
    Spring-Boot-应用可视化监控
    How to Convert a Class File to a Java File?
    Nacos Cluster Building
    基于Opentracing+Jaeger全链路灰度调用链
    Opentracing + Uber Jaeger 全链路灰度调用链,Nepxion Discovery
    What happened when new an object in JVM ?
    Sentinel Getting Started And Integration of Spring Cloud Alibaba Tutorials
    APK重签名总结
    C++矩阵运算库推荐
  • 原文地址:https://www.cnblogs.com/pangblog/p/3304064.html
Copyright © 2011-2022 走看看