链表反转:有两种方式 - 普通、递归


2013-4-11,周四,搜狗笔试题:单链表反转。这个题是经常出现的一个笔试和面试题。

题目:输入一个链表的头结点,反转该链表,并返回反转后链表的头结点

分析:这是一道广为流传的微软面试题。由于这道题能够很好的反应出程序员思维是否严密,在微软之后已经有很多公司在面试时采用了这道题。

为了正确地反转一个链表,需要调整指针的指向。与指针操作相关代码总是容易出错的,因此最好在动手写程序之前作全面的分析。在面试的时候不急于动手而是一开始做仔细的分析和设计,将会给面试官留下很好的印象,因为在实际的软件开发中,设计的时间总是比写代码的时间长。与其很快地写出一段漏洞百出的代码,远不如用较多的时间写出一段健壮的代码。

为了将调整指针这个复杂的过程分析清楚,我们可以借助图形来直观地分析。假设l、m和n是三个相邻的结点:

a b …l m n …

假设经过若干操作,我们已经把结点l之前的指针调整完毕,这些结点的 pNext 指针都指向前面一个结点。现在我们遍历到结点m。当然,我们需要把调整结点的 pNext 指针让它指向结点l。但注意一旦调整了指针的指向,链表就断开了,如下所示:

a b…l m n…

因为已经没有指针指向结点n,我们没有办法再遍历到结点n了。因此为了避免链表断开,我们需要在调整m的 pNext 之前要把n保存下来。

接下来我们试着找到反转后链表的头结点。不难分析出反转后链表的头结点是原始链表的尾位结点。什么结点是尾结点?就是 pNext 为空指针的结点。

 

示例代码:

package com.suanfa.lianbiao;

class Node<E> {

	// 数据
	public E data;
	// 指向下一个节点
	public Node<E> next;

	public Node(E data) {
		this.data = data;
	}

	// public Node<E> getNext() {
	// return next;
	// }
	//
	// public void setNext(Node<E> next) {
	// this.next = next;
	// }

}

class LinkList<E> {

	public Node<E> first;

	// 链表中数据项的个数
	public int size;

	public LinkList() {
		first = null;
		size = 0;
	}

	// 在表头插入新的数据
	public void insertFirst(E value) {
		Node<E> link = new Node<E>(value);
		link.next = first;
		first = link;
		size++;
	}

	// 判断链表是否为空
	public boolean isEmpty() {
		return size == 0;
	}

	// 删除表头
	public Node<E> deleteFirst() {
		Node<E> temp = first;
		first = first.next;
		size--;
		return temp;
	}

	// 输出链表中的所有数据
	public void display() {
		Node<E> curr = first;
		while (curr != null) {
			System.out.print(curr.data + " ");
			curr = curr.next;
		}
		System.out.println();
	}

	// 返回链表中数据项的个数
	public int size() {
		return size;
	}

	// 获取从头至尾的第i个数据项
	public Node<E> get(int i) {
		if (i > size() - 1 || i < 0) {
			try {
				throw new IndexOutOfBoundsException();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		Node<E> curr = first;
		for (int n = 0; n < size(); n++) {
			if (n == i) {
				return curr;
			} else {
				curr = curr.next;
			}
		}
		return null;
	}

	// 输出从头至尾的第i个数据项
	public void remove(int i) {
		if (i == 0) {
			deleteFirst();
		} else if (i == size() - 1) {
			get(i - 1).next = null;
		} else {
			get(i - 1).next = get(i + 1);
		}
		size--;
	}

	// 其实下面这2个反转的方法原理都是一样的,就是有的把第一个节点当做cur,有的把第二个节点当做cur
	// 两种方式实现单链表的反转(递归、普通)
	public Node<E> reverse_1(Node<E> head) {
		if (head == null) {
			return null;
		}
		// 链表反转后的新头结点
		Node<E> reverseHead = null;
		Node<E> cur = head;
		Node<E> pre = null;

		while (cur != null) {
			Node<E> next = cur.next;
			if (next == null) {
				reverseHead = cur;
			}

			cur.next = pre;
			pre = cur;
			cur = next;
		}
		return reverseHead;
	}

	/**
	 * 遍历,将当前节点的下一个节点缓存后更改当前节点指针
	 * 
	 */
	public Node<E> reverse_2(Node<E> head) {
		if (null == head) {
			return head;
		}
		Node<E> pre = head;
		Node<E> cur = head.next;
		Node<E> next;
		while (null != cur) {
			next = cur.next;
			cur.next = pre;
			pre = cur;
			cur = next;
		}
		// 将原链表的头节点的下一个节点置为null,再将反转后的头节点赋给head
		head.next = null;
		head = pre;

		return head;
	}

	/**
	 * 递归,在反转当前节点之前先反转后续节点
	 */
	public Node<E> reverse_3(Node<E> head) {
		if (null == head || null == head.next) {
			return head;
		}

		Node<E> reversedHead = reverse_3(head.next);
		head.next.next = head;
		head.next = null;
		return reversedHead;
	}

}

public class Link_list {
	public static void main(String[] args) {
		LinkList<Long> ll = new LinkList<Long>();

		// 构造链表
		for (int i = 0; i < 10; i++) {
			Long value = (long) (Math.random() * 100);
			ll.insertFirst(value);
		}

		ll.display();

		while (!ll.isEmpty()) {
			ll.deleteFirst();
			ll.display();
		}

		System.out.println("Ok");
	}
}


 

延伸阅读:

class Node
  {
  Object data;
  Node next;  //指向下一个结点
  }
  将数据域定义成Object类是因为Object类是广义超类,任何类对象都可以给其赋值,增加了代码的通用性。为了使链表可以被访问还需要定义一个表头,表头必须包含指向第一个结点的指针和指向当前结点的指针。为了便于在链表尾部增加结点,还可以增加一指向链表尾部的指针,另外还可以用一个域来表示链表的大小,当调用者想得到链表的大小时,不必遍历整个链表。

 

需要反复思考,总结,消化为自己的知识。

 

参考链接:

http://blog.163.com/sparkle_tiangz/blog/static/11759020320102125278750/

 

 

 

你可能感兴趣的:(链表反转:有两种方式 - 普通、递归)