您的位置:首页 > 理论基础 > 数据结构算法

【数据结构】链表

2015-06-30 22:32 561 查看
链表是数据结构课程的第一讲,也是最为简单的数据结构。其基本结构是一个包含有值和另一个节点地址或索引的对象。逐个对象因为上一级(前驱)的索引而一一相连,形成了一个链状的线性结构。链表可以灵活地增加或者减少节点的个数,当时需要增加时,临时向系统申请一块内存,并建立索引。因此与数组不同,链表的节点可以分布于内存中的任何地方,它们并不是一个有序相邻放置的结构尽管在程序应用上,我们将其视为一个线性表。因为节点放置的分散,所以在访问时,指针势必会高频率跳转,这也使得访问的耗时在硬件上层面上是大于数组的访问的。

链表根据索引的方向和个数不同,可以建立从前至后的单向链表,前后相互索引的双向链表,起始和结尾相连的循环链表等等(如下图)。



其结构灵活足以满足各类工业开发,并且也是面试中考官们乐此不疲的话题点。本文讲给出单向链表的代码实现。

代码实现

单向链表类型中保存有首节点地址作为头指针,这是链表的入口地址。如下给出链表类定义

class LinkedList{
	Node head;
}


如下代码给出一个单向链表节点的定义

class Node {
	int value;
	Node next;

	public Node(int value) {
		this.value = value;
	}
}


如下代码给出链表的增删改查操作的成员函数定义。

插入操作:传入插入值

public void insert(int value) {
		if (head == null)
			head = new Node(value);
		else {
			Node pointer = head;
			while (pointer.next != null)
				pointer = pointer.next;
			pointer.next = new Node(value);
		}
	}


删除:删除指定索引

public boolean delete(int position) {
		/**
		 * 参数验证,必不可少
		 */
		if (position < 0)
			return false;
		if (head == null)
			return false;

		if (position == 0) {
			if (head != null)
				head = head.next;
		} else {
			int n = 0;
			Node previous = head;
			Node current = head.next;
			while (current != null) {
				previous = current;
				current = current.next;
				n++;
				if (n == position - 1) {
					if (current != null) {
						previous.next = current.next;
						current.next = null;
					} else
						previous.next = null;
				}
			}
			if (n < position - 1)
				return false;
		}
		return true;
	}


查找:获取某一索引的值

public int get(int position) throws NoSuchElementException{
		Node current = head;
		int n = 0;
		while(current != null){
			if(position == n) return current.value;
			current = current.next;
			n++;
		}
		throw new NoSuchElementException();
	}


查找:验证是否包含某一值

public int contains(int value) {
		Node current = head;
		int n = 0;
		while (current != null) {
			if (current.value == value)
				return n;
			current = current.next;
			n++;
		}
		return -1;
	}


修改:修改某一索引的值

public void set(int position, int value) {
		Node current = head;
		int n = 0;
		while(current != null){
			if(n == position) current.value = value;
			current = current.next;
			n++;
		}
	}


由于单向链表的插入只发生在尾部,因此链表元素的存储是以插入顺序保存的,所谓先来后到。对于单向链表的排序,需要考虑到其正向索引可行而反向索引局限的问题,因此冒泡,插入排序等将很难实现。我在学习链表时,课本推荐选择排序,这里给出单线链表选择排序的参考代码

/**
	 * 选择排序
	 */
	public void selectionSort() {
		if (head != null || head.next != null) {
			Node current = head;
			Node next = null;
			Node minimum = null;
			int temp = 0;
			
			while (current != null) {
				next = current.next;
				minimum = current;
				while (next != null) {
					if (next.value < minimum.value) {
						minimum = next;
					}
					next = next.next;
				}
				
				temp = current.value;
				current.value = minimum.value;
				minimum.value = temp;
				
				current = current.next;
				minimum = current;
			}
		}
	}


完整代码如下

class Node {
	int value;
	Node next;

	public Node(int value) {
		this.value = value;
	}
}

class LinkedList {
Node head;

/**
* 插入数值
*/
public void insert(int value) { if (head == null) head = new Node(value); else { Node pointer = head; while (pointer.next != null) pointer = pointer.next; pointer.next = new Node(value); } }

/**
* 删除指定位置的节点
*/
public boolean delete(int position) { /** * 参数验证,必不可少 */ if (position < 0) return false; if (head == null) return false; if (position == 0) { if (head != null) head = head.next; } else { int n = 0; Node previous = head; Node current = head.next; while (current != null) { previous = current; current = current.next; n++; if (n == position - 1) { if (current != null) { previous.next = current.next; current.next = null; } else previous.next = null; } } if (n < position - 1) return false; } return true; }

/**
* 修改指定节点的值
*/
public void set(int position, int value) {
Node current = head;
int n = 0;
while (current != null) {
if (n == position)
current.value = value;
current = current.next;
n++;
}
}

/**
* 查找某一索引的值
*/
public int get(int position) throws NoSuchElementException{ Node current = head; int n = 0; while(current != null){ if(position == n) return current.value; current = current.next; n++; } throw new NoSuchElementException(); }

/**
* 验证是否包含某一值
*/
public int contains(int value) { Node current = head; int n = 0; while (current != null) { if (current.value == value) return n; current = current.next; n++; } return -1; }

/** * 选择排序 */ public void selectionSort() { if (head != null || head.next != null) { Node current = head; Node next = null; Node minimum = null; int temp = 0; while (current != null) { next = current.next; minimum = current; while (next != null) { if (next.value < minimum.value) { minimum = next; } next = next.next; } temp = current.value; current.value = minimum.value; minimum.value = temp; current = current.next; minimum = current; } } }
}


如上是一个基本链表的实现过程,当然这里是针对整型的链表,根据需要可以在链表中加入长度标量来记录当前链表的长度,也可以加入记录最大最小值的变量等。

J***A中,容器库提供了java.util.LinkedList这一个泛型链表容器, 其本质是一个双向链表。与此功能类似的还有一个java.util.ArrayList容器,虽然它不是一个链表容器,但却是一个与链表功能类似的线性表容器,它使用拼接数组的方式实现了容量的灵活变化。由于其容器本质是一个数组,因此访问效率高于链表。然而由于其扩充数组容量是通过新建一个更大数组并复制原有数据的过程来实现,所以这个操作略显耗时。总的来说,两种容器各有优劣,代码的实现也很具有学习价值。

了解java.util.LinkedList和java.util.ArrayList的源码可以参考我的文章《【源代码】java.util.LinkedList》《【源代码】java.util.ArrayList》

面试常见问题

我前边说过,链表是面试时考官的大爱,那么这里总结一些面试常见链表问题供大家参考和讨论。

1. 仅给定一个单项链表中间的某一节点索引(或地址),在不知道头指针的情况下删除这个节点。

这个问题很常见。我们知道单链表中当前节点有且仅有后继(下一节点)的地址,在不知道头节点地址的情况下,无法获取某一节点的前驱。所以考虑如下情况,我们可以考虑删除指定节点的后继,在删除之前复制后继的值到当前节点即可。

2. 如何检查一个单向链表中是否有环(某一节点的后继为某一前驱)。

可以考虑两个指针遍历链表,一个指针步进为1,另一指针步进为2,也就是一个一次遍历1个next索引,另一个一次遍历2个next索引。如果某一时刻两个指针相遇则代表有环,如果某一指针发现链表结尾则该链表没有环。

3. 给定两个单向链表头指针,确定两个链表是否相交。

要知道单向链表一旦相交,则交点之后的链表为两者共享。那么我们可以定义两个指针,同时遍历两根链表,每次循环两个指针同时向后移动一个索引,当索引到的对象为统一对象时则发现交点。那么如何保证两个指针能够同时到达交点呢?可以使长度较短的链表由头结点开始,长度较长的由索引为(长链表长度 - 短链表长度)的节点开始同时移动即可。

4. 给定一个单项链表的头指针,就地翻转每一个节点的索引方向,即首尾倒置。

这里提供一个解决方案。建立三个索引,分别代表当前节点,前驱和后继,每次循环改变当前节点的next索引到前驱,并逐一将三个索引后移一位,最后完成链表倒置操作。其中有不少陷阱需要处理。一下是我的代码,可以加在之前自定义的LinkedList类中作为成员方法。

public void reverse(){
		if(head != null || head.next != null){
			Node previous = null;
			Node current = head;
			Node next = current.next;
			
			while(next != null){
				current.next = previous;
				previous = current;
				current = next;
				next = next.next;
			}
			current.next = previous;
			head = current;
		}
	}


5. 复制一个复杂单链表(节点中包含不止一个索引,并且索引无序)。

下图是一个复杂单链表示意图。这里假设节点中包含三个成员变量,值、后继引用和其他节点引用。



这里提供我的一个解决方案。逐一遍历每个节点,复制单链表的主干,其他节点引用先空置。同时用一个哈希表以<当前序号,引用序号>的形式将当前节点的序号和其对应的其他节点序号存储起来。这个听起来有点复杂,以上图为例,节点4的其他节点指向节点1,所以这个key-value-pair就为4->1。

开始复制时也是先复制主干,逐一得到一个新的单链表。然后根据刚才的哈希表可以得到当前新节点应该指向的索引值。

当然了具体的代码还是需要手上过的,写下来才是王道,各位网友不妨去试试,欢迎交流指正。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: