面试算法之链表操作集锦

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

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

    原文作者:walker沃克
    原文地址: https://blog.csdn.net/anonymalias/article/details/11020477
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞