1. 概述
前面说到了数组,利用连续的内存空间来存储相同类型的数据,其最大的特色是支持下标随机访问,可是删除和插入的效率很低。今天来看另外一种很基础的数据结构——链表。链表不须要使用连续的内存空间,它使用指针将不连续的内存块链接起来,造成一种链式结构。node
2. 单链表
首先来看看单链表,存储数据的内存块叫作节点,每一个节点保存了一个 next 指针,指向下一个节点的内存地址,结合下图你就很容易看明白了:
其中有两个节点指针比较的特殊,首先是链表头节点的指针,它指向了链表的第一个节点的地址,利用它咱们能够遍历获得整个链表。其次是尾结点的指针,它指向了 null ,表示链表结束。数组
不难看出,单链表的最大特色即是使用指针来链接不连续的节点,这样咱们不用担忧扩容的问题了,而且,链表的插入和删除操做也很是的高效,只须要改变指针的指向便可。
结合上图不难理解,单链表可以在 O(1) 复杂度内删除和添加元素,这就比数组高效不少。可是,若是咱们要查找链表数据怎么办呢?链表的内存不是连续的,不能像数组那样根据下标访问,因此只能经过遍历链表来查找,时间复杂度为 O(n)。下面是单链表的代码示例:数据结构
public class SingleLinkedList { private Node head = null;//链表的头节点 //根据值查找节点 public Node findByValue(int value) { Node p = head; while (p != null && p.getData() != value) p = p.next; return p; } //根据下标查找节点 public Node findByIndex(int index) { Node p = head; int flag = 0; while (p != null){ if (flag == index) return p; flag ++; p = p.next; } return null; } //插入节点到链表头部 public void insertToHead(Node node){ if (head == null) head = node; else { node.next = head; head = node; } } public void insertToHead(int value){ this.insertToHead(new Node(value)); } //插入节点到链表末尾 public void insert(Node node){ if (head == null){ head = node; return; } Node p = head; while (p.next != null) p = p.next; p.next = node; } public void insert(int value){ this.insert(new Node(value)); } //在某个节点以后插入节点 public void insertAfter(Node p, Node newNode){ if (p == null) return; newNode.next = p.next; p.next = newNode; } public void insertAfter(Node p, int value){ this.insertAfter(p, new Node(value)); } //在某个节点以前插入节点 public void insertBefore(Node p, Node newNode){ if (p == null) return; if (p == head){ insertToHead(newNode); return; } //寻找节点p前面的节点 Node pBefore = head; while (pBefore != null && pBefore.next != p){ pBefore = pBefore.next; } if (pBefore == null) return; newNode.next = pBefore.next; pBefore.next = newNode; } public void insertBefore(Node p, int value){ insertBefore(p, new Node(value)); } //删除节点 public void deleteByNode(Node p){ if (p == null || head == null) return; if (p == head){ head = head.next; return; } Node pBefore = head; while (pBefore != null && pBefore.next != p){ pBefore = pBefore.next; } if (pBefore == null) return; pBefore.next = pBefore.next.next; } //根据值删除节点 public void deleteByValue(int value){ Node node = this.findByValue(value); if (node == null) return; this.deleteByNode(node); } //打印链表的全部节点值 public void print(){ Node p = head; while (p != null){ System.out.print(p.getData() + " "); p = p.next; } System.out.println(); } //定义链表节点 public static class Node{ private int data; private Node next; public Node(int data) { this.data = data; this.next = null; } public int getData() { return data; } } }
3. 循环链表
循环链表和单链表的惟一区别即是链表的尾结点指针并非指向 null,而是指向了头节点,这样便造成了一个环形的链表结构:this
4. 双向链表
双向链表,顾名思义,就是链表不仅是存储了指向下一个节点的 next 指针,还存储了一个指向前一个节点的 prev 指针,以下图:
为何要使用这种具备两个指针的链表呢?主要是为了解决链表删除和插入操做的效率问题。spa
在单链表中,要删除一个节点,必须找到其前面的节点,这样就要遍历链表,时间开销较高。可是在双向链表中,因为每一个节点都保存了指向前一个节点的指针,这样咱们可以在 O(1) 的时间复杂度内删除节点。指针
插入操做也相似,好比要在节点 p 以前插入一个节点,那么也必须遍历单链表找到 p 节点前面的那个节点。可是双向链表能够直接利用前驱指针 prev 找到前一个节点,很是的高效。code
这也是双向链表在实际开发中用的更多的缘由,虽然每一个节点存储了两个指针,空间开销更大,这就是一种典型的用空间换时间的思想。blog
下面是双向链表的代码示例:图片
public class DoubleLinkedList { private Node head = null;//链表的头节点 //在某个节点以前插入节点,这里方能体现出双向链表的优点 public void insertBefore(Node p, Node newNode) { if (p == null) return; if(p == head) { this.insertToHead(newNode); return; } newNode.prev = p.prev; p.prev.next = newNode; newNode.next = p; p.prev = newNode; } public void insertBefore(Node p, int value) { this.insertBefore(p, new Node(value)); } //删除某个节点 public void deleteByNode(Node node) { if(node == null || head == null) return; if (node == head) { head = head.next; if(head != null) head.prev = null; return; } Node prev = node.prev; Node next = node.next; prev.next = next; if(next != null) next.prev = prev; } //根据值删除节点 public void deleteByValue(int value) { Node node = this.findByValue(value); if (node == null) return; this.deleteByNode(node); } //定义链表节点 public static class Node{ private int data; private Node prev;//链表的前驱指针 private Node next;//链表的后继指针 public Node(int data) { this.data = data; this.prev = null; this.next = null; } public int getData() { return data; } } }