线性表之链表原理与实现

1、链表的存储结构

  之前我们讲过顺序表,顺序表的特点就是数据之间逻辑上的相邻关系在物理位置上也是相邻的,因此它可以通过寻址公式随机存取任意一个元素,但是这种特点也为元素的删除和插入带来了极大的不便。链表的特点就是:元素之间逻辑上的相邻关系在物理位置上并不一定相邻,元素的物理地址可以是任意一个合法的地址,这些元素通过指针联系在一起形成了一条链式结构,这样的结构在插入和删除操作时就不需要挪动元素位置,只是通过简单的修改指针的指向就可以了,因此理论上它的时间复杂度为O(1),同时,由于这种特性,因此每个元素的屋里地址是随机的,无法通过寻址公式来定位故而也失去了随机存取的特点。
  通过上面的描述我们可以总结出链表中的元素用任意的存储单元来存储它的信息。由于我们需要利用指针来描述元素之间的关系,因此存储单元除了存储元素本身的数据信息外,还必须存储描述数据之间关系的信息,也就是指针域,通过这些指针将每个独立的元素联系起来。我们将这些零散的内存块称之为结点。因此,链表中的结点主要包含两个信息—数据域和指针域 。链表有多种不同的结构,根据指针的指向可以将链表分为单链表、双链表和循环链表。

2、单链表

  所谓单向链表就是链表中的每一个数据项都包含数据域和一个指针域,这个指针指向下一个结点,因此这个指针域又称为后继指针。我们用一个图来形象地表示。
在这里插入图片描述
我们可以看到,链表中有两个结点比较特殊,分别为第一个结点和最后一个结点,其中第一个结点我们称之为头结点,最后一个结点称之为尾结点。头结点用于指向链表的基地址,也就是链表中第一个存储元素的地址,通过头结点我们就能遍历整条链表,通常头结点的数据域可以不存储任何数据也可以存储链表的长度等信息。尾结点用于指向一个NULL,表示此结点是整个链表的最后一个结点。
  我们来看看单链表的插入和删除操作。其实链表的插入和删除操作非常简单,它不需要搬运元素,只需要改改指针指向即可。我们也来看看一个图示
在这里插入图片描述
假如现在我们需要将p指向的结点插入到当前结点q的后面,只需做如下操作即可:p->next = q->next; q -> next = p;我们可以看到实际上插入一个元素只是简单地改改指针指向即可。删除操作也一样,只是在删除操作时需要注意的一点是如果是用像C语言这样手动管理内存的语言,在删除结点后必须手动释放掉这块内存。

3、循环链表

  在单链表中我们知道,链表的尾结点指向一个NULL,而循环链表就是尾结点的指针指向链表中第一个元素
在这里插入图片描述
其实没什么可说的,一目了然。

4、双向链表

  双向链表就是每一个结点除了数据域和后继指针外,还有一个指针这个指针用于指向前驱结点,所以又叫前驱指针,因此它的存储结构如下图所示
在这里插入图片描述
我们可以看到双向链表比单链表多了一个指针域,因此在同样多的元素的情况下,双向链表所占用的内存会比单向链表更大。但是双向链表却比但链表多一个特点就是它支持双向遍历,从结构上来看它可以在O(1)的时间复杂度找到某个给定结点的直接前驱结点。而单链表如果给定了某个结点去寻找它的直接前驱结点就需要从头遍历因此时间复杂度为O(n)。

4、再谈链表的插入和删除

  上面我们讲过链表的插入和删除操作在理论上来讲时间复杂度为O(1),为什么是理论上的复杂度呢?
  我们先来看看插入操作,在实际的开发中我们在链表中插入一个数据主要有两种:其一是将某个结点插入链表中某个给定的值,例如将5这个元素插入到4这个结点之后;其二是将某个结点插入到给定指针所指向的结点后面。对于第二种情况它确实只需要O(1)的时间复杂度,可是对于第一种情况来说就有些不同了,首先你必须遍历这个链表找到这个结点从而插入到具体的位置,那么这样一来如果是仅仅插入本身来说只需要O(1)的复杂度,但是整个过程却需要O(n)的复杂度。
  再来看看删除操作,从链表删除一个元素同样也有两种:其一是删除结点中值等于某个给定值的结点;其二是删除给定指针指向的结点。对于第一种情况,为了找到这个给定值的结点也必须先遍历这个链表,然后再通过操作指针删除这个元素结点。因此时间复杂度为O(n)。对于第二种情况,首先我们已经知道了需要删除的结点的指针,通过前面的分析,要删除这个结点就必须知道这个结点的直接前驱结点。因此对于单链表而言,为了找到这个直接前驱结点,必须还得从链表的头结点遍历从而找到待删除结点的直接前驱结点然后再通过操作指针进行删除,因而时间复杂度也为O(n)。但是对于双向链表而言就有些不同了,由于双向链表的结点存储了直接前驱结点,因此在对第二种情况删除时就不需要再遍历一遍链表,直接操作指针即可,其时间复杂度为O(1)。同样,假如我们需要在某个结点之前插入,单链表也需要从头遍历,而双向链表则可以直接操作指针插入。
  因此我们可以看出双向链表在插入和删除操作中比单链表更加灵活,高效。因而在实际的开发中,究竟选择哪种结构要根据实际情况而定,而上面的双向链表的选择虽然操作更加灵活,但是增加了内存的消耗,是一种用空间来换时间的策略。

5、数组和链表

  通过前面的分析我们已经知道了数组和链表各自的优缺点,各自在操作上的时间复杂度。但是在实际的开发中我们不能仅仅通过复杂度这唯一的因素来决定使用哪种数据结构。
  我们知道数组的有点是内存地址连续可以随机存取,由于内存连续,因此数组对于CPU而言更加友好。我们知道优于CPU的效率要高于内存,因此CPU在内存中读取数据时往往不是只读取要用的那部分内存数据,而是读取一个数据块,将相邻的部分数据也读入,因此它的效率会更高,这就是cpu的缓存机制。而链表它由于内存分散,所以没办法读入其他相邻的数据,因此链表对cpu并不友好。可正因为这个原因,数组的大小固定,如果没有预先的判断,申请的数组容量太小可能会导致数组在存储时,内存不足。如果内存不足,你就得重新申请一块更大的内存区域将原来的数组copy进去,这样非常费时。而对于链表而言由于它的内存区域本来就是独立分散的,因此它天生就具有扩容的机制,因此它在动态性上要优于数组,但是也正因为这个特性,每次新增加一个结点都必须像系统申请分配内存,这样频繁地申请内存无疑也加大了系统的开销。我们列出了这么多链表和数组的不同特点,可见在开发中选择什么样的数据结构需要考虑的因素挺多的,但是有得必有舍,还是要看你的诉求是什么。

6、链表的实现(Java)

  讲了那么多,我们也给出代码实现一下链表的机构,这里我只给出单链表的实现,以整数类型为例未使用泛型。

public class MyLinkList {
	// 头结点,不存储任何数据
	private Node head;
	// 构造方法
	public MyLinkList() {
		// 创建一个头结点
		head = new Node();
	}
	class Node {
		int data; // 数据域
		Node next; // 指针域
		// 无参构造方法
		public Node() {
			next = null;
		}
		// 有参构造方法
		public Node(int data) {
			this.data = data;
			this.next = null;
		}
	}
	
	/**
	* 在链表头部插入数据
	* @param
	*/
	public void addToHead(int value) {
		// 创建一个新的结点
		Node newNode = new Node(value);
		if (head.next != null) {
			// 如果链表中还已经插入元素
			newNode.next = head.next;
			head.next = newNode;
		}
		else {
			//链表中还未插入任何元素
			head.next = newNode;
		}
	}
	
	/**
	* 在链表的尾部插入数据
	* @param value
	*/
	public void addToTail(int value) {
		// 创建一个新的结点
		Node newNode = new Node(value);
		// 如果链表中已经插入数据
		if (head.next != null) {
			Node temp = head;
			// 遍历链表找到最后一个结点
			while (head.next != null) {
				temp = temp.next;
			}
			temp.next = newNode;
		}
		else {
		// 链表中还未插入任何数据
		head.next = newNode;
		}
	}
	
	/**
	* 在链表的指定位置插入元素,不管给出的位置是否合法都找到合适的位置插入
	* @param pos
	* @param value
	*/
	void insert(int pos, int value) {
		Node newNode = new Node(value);
		if (head.next != null) {
			Node temp = head;
			for (int i=0; i < pos-1; i++) {
				// 检测当前结点的后继是否为空,为空则终止循环
				// 即使给出的位置大于链表长度,也将此元素插入到最后一个位置
				// 如果给定的位置小于等于0,则将它插入到第一个位置
				if (temp.next != null)
					temp = temp.next;
				else break;
			}
			newNode.next = temp.next;
			temp.next = newNode;
		}
		else {
			// 如果链表中未插入数据
			head.next = newNode;
		}
	}

	/**
	* 获取链表的长度
	*/
	public int getLength() {
		Node temp = head;
		for (int length = 0; ;length++) {
			if (temp.next == null)
				return length;
			temp = temp.next;
		}
	}

	/**
	* 删除指定位置上的元素
	* @pos
	*/
	public void removeNode(int pos) {
		if (head.next == null)
			return;
		else {
			Node prev;
			Node curNode = head;
			for (int i = 0; i < pos; i++) {
				if (curNode.next != null) {
					prev = curNode;
					curNode = curNode.next;
				}
				else break;
			}
			// 删除操作
			if (pos > 0) {
				prev.next = curNode.next;
				curNode.next = null;
			}
			else {
				curNode = curNode.next;
				head.next = curNode.next
				curNode.next = null;
			}
		}
	}
}
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 深蓝海洋 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读