[本专题会对常见的数据结构及相应算法进行分析与总结,并会在每一个系列的博文中提供几道相关的一线互联网企业面试/笔试题来巩固所学及帮助咱们查漏补缺。项目地址:https://github.com/absfree/Algo。因为我的水平有限,叙述中不免存在不清晰准确的地方,但愿你们能够指正,谢谢你们:)] node
提到链表,咱们你们都不陌生,在平时的编码中咱们也或多或少地使用过这个数据结构。算法(第4版) (豆瓣)一书中对链表的定义以下:git
链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点(node)的引用,该节点还有一个元素和一个指向另外一条链表的引用。github
把以上定义用Java语言来描述大概是这样的:面试
public class LinkedList<Item> { private Node first; private class Node { Item data; Node next; } ... }
一个LinkedList类实例便表明了一个链表,它的一个实例域保存了指向链表中第一个结点的引用。以下图所示:算法
固然,以上咱们所介绍的链表是single linked list(单向链表),有时候咱们更喜欢double linked list(双向链表),double linked list就是每一个node不只包含指向下后一个结点的引用,还包含着指向前一个结点的引用。后文咱们在介绍链表的具体实现是会对这两种链表进行更加详细地介绍。编程
一般来讲,链表支持插入和删除这两种操做,而且删除/插入链表头部/尾部结点的时间复杂度一般都是常数级别的,链表的不足在于不支持高效的random access(随机访问)。缓存
在上文中,咱们已经简单用用Java刻画出了链表的部分结构,咱们只需为以上的LinkedList类增长insert、delete等方法,即可以实现一个(单向)链表。下面咱们来介绍如何向链表中插入及删除结点。数据结构
因为咱们的LinkedList类中维护了一个指向first node的引用,因此在表头插入结点是很容易的,具体请看如下代码:app
public void insert(Item item) { Node oldFirst = first; first = new Node(); first.item = item; first.next = oldFirst; itemCount++; }
在表头删除结点的代码也很简单,基本是自注释的:框架
public Item delete() { if (first != null) { Item item = first.item; first = first.next; return item; } else { throw new NullPointerException("There's no Node in the linked list."); }
双向链表相比与单链表的优点在于它同时支持高效的正向及反向遍历,而且能够方便的在链表尾部删除结点(单链表能够方便的在尾部插入结点,但不支持高效的表尾删除操做)。双向链表的Java描述以下:
public class DoubleLinkedList<Item> { private Node first; private Node last; private int itemCount; private class Node { Node prev; Node next; Item item; } public void addFirst(Item item) { Node oldFirst = first; first = new Node(); first.item = item; first.next = oldFirst; if (oldFirst != null) { oldFirst.prev = first; } if (itemCount == 0) { last = first; } itemCount++; } public void addLast(Item item) { Node oldLast = last; last = new Node(); last.item = item; last.prev = oldLast; if (oldLast != null) { oldLast.next = last; } if (itemCount == 0) { first = last; } itemCount++; } public Item delFirst() { if (first == null) { throw new NullPointerException("No node in linked list."); } Item result = first.item; first = first.next; if (first != null) { first.prev = null; } if (itemCount == 1) { last = null; } itemCount--; return result; } public Item delLast() { if (last == null) { throw new NullPointerException("No node in linked list."); } Item result = last.item; last = last.prev; if (last != null) { last.next = null; } if (itemCount == 1) { first = null; } itemCount--; return result; } public void addBefore(Item targetItem, Item item) { //从头开始遍历寻找目标节点 Node target = first; if (target == null) { throw new NullPointerException("No node in linked list"); } while (target != null && target.item != targetItem) { //继续向后寻找目标节点 target = target.next; } if (target == null) { throw new NullPointerException("Can't find target node."); } //如今target为指向目标结点的引用 if (target.prev == null) { //此时至关于在表头插入结点 addFirst(item); } else { Node oldPrev = target.prev; Node newNode = new Node(); newNode.item = item; target.prev = newNode; newNode.next = target; newNode.prev = oldPrev; oldPrev.next = newNode; itemCount++; } } public void addAfter(Item targetItem, Item item) { Node target = first; if (target == null) { throw new NullPointerException("No node in linked list."); } while (target != null && target.item != targetItem) { target = target.next; } if (target == null) { throw new NullPointerException("Can't find target node."); } if (target.next == null) { addLast(item); } else { Node oldNext = target.next; Node newNode = new Node(); newNode.item = item; target.next = newNode; newNode.prev = target; newNode.next= oldNext; oldNext.prev = newNode; itemCount++; } } }
上面代码的逻辑都很直接,不过刚接触链表的小伙伴有时候可能容易感到有些迷糊,这时候一个好方法即是在拿出笔纸,画出链表操做相关结点的prev、next指针等的指向变化状况,这样链表相关的各种操做过程都能被很是直观的展示出来。
有一点须要咱们注意的是,咱们上面实现链表使用的是pointer wrapper方式,这种方式的特色是prev/next指针包含在结点中,而数据由结点中的另外一个指针(即item)所引用。采起这种方式,在获取结点数据时,咱们须要进行double-dereferencing,并且这种方式实现的链表不是一种[局部化结构],这意味着咱们拿到链表的一个结点数据后,没法直接进行insert/delete操做。
另外一种实现链表的方式是intrusive方式,这种方式实现的链表也就是intrusive linked list。这种链表的特色是data就是node,node就是data。使用这种链表,咱们在获取data时,无需double-dereferencing,而且intrusive linked list是一种局部结构。
链表的主要优点有两点:一是插入及删除操做的时间复杂度为O(1);二是能够动态改变大小。
因为其链式存储的特性,链表不具有良好的空间局部性,也就是说,链表是一种缓存不友好的数据结构。
下面咱们从《剑指Offer》中挑出几道关于链表的经典面试题来进一步巩固咱们对链表相关技术点的掌握。
这道题给咱们的框架以下,咱们要作的是在这个框架中编程来实现从头至尾打印链表:
/* public class ListNode { int val; ListNode next = null; ListNode(int val) { this.val = val; } }*/ public class Solution { public ListNode FindKthToTail(ListNode head,int k) { } }
首先咱们能够看到这里面表示链表的是ListNode类,这对应着咱们上面的单链表实现中的Node类。实际上,这道题的难度要比咱们上面实现的DoubleLinkedList中的addBefore/addAfter方法的难度要小。
我想到的一种直接解法以下:(若有问题但愿你们能够指出)
public class Solution { public ListNode FindKthToTail(ListNode head,int k) { //先求得链表的尺寸,赋值给size int size = 0; ListNode current = head; while (current != null) { size++; current = current.next; } //获取next实例域size-k次,便可获得倒数第k个结点(从1开始计数) for (int i = 0; i < size - k; i++) { head = head.next; } return head; } }
本题的要求是输入一个链表,反转链表后,输出链表的全部元素。这道题的实现也比较直接,如如下代码所示:
public ListNode ReverseList(ListNode head) { if (head == null) { return null; } ListNode current = head; //原head的next node为null ListNode prevNode = null; ListNode newHead = null; while (current != null) { ListNode nextNode = current.next; current.next = prevNode; if (nextNode == null) { newHead = current; } prevNode = current; current = nextNode; } return newHead; }
这里只是从剑指Offer中找了两道关于链表的题来练手,之后会陆续在上面提到的项目地址跟你们分享更多的常常被用来做为一线互联网公司面试/笔试题的题目,这样在巩固本身算法基本功的同时,在面试/笔试时也可以更加驾轻就熟。