赞
踩
在数组存储的过程中,数组的存储必须使用连续的内存空间,并且会预留一部分空间方便扩展。这样在内存中会大大降低内存的使用率,这种事情是不被接受的。所以出现了一种新的数据结构,叫作——链表
综上可知:链表的发明其实是对数组的一种补充。因此它的功能和数组非常类似,都是用于存储一系列相同类型的数组,并且都有增删改查等基本操作
用一句话来总结:
数组能做的所有事情,一般都可以用链表实现
只是他们的存储方式不同,导致他们的实现逻辑也不同,同样时间复杂度也将不同。下面我们来分析一下链表的特性:
节点
数组里面每个元素我们称之为元素,而链表中每个元素我们称为节点。
从图中看出,每个节点都是由两部分组成 — 节点内容和下一个节点地址。元素和节点的差别就在这个下一个节点地址
在数组中,由于是连续内存空间存储,我们可以通过
start_address + item_size * i
计算元素的位置。而在链表中,存储空间是分散存储的,所以我们需要每个节点保存下一个节点的位置。也就是可以通过节点 1 找到节点 2,节点 2 找到节点 3…依次遍历所有的节点信息
从这个结构,可以看出链表对于数组最大的好处,它可以将数据分散存储在内存中,达到内存最大利用率
代码创建链表:
public class Node { private int content; private Node next; public Node() { } public Node(int content, Node next) { this.content = content; this.next = next; } public int getContent() { return content; } public void setContent(int content) { this.content = content; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } }
// 创建链表 public static Node createLinkedList(int[] array) { // 创建一个根节点 Node root = null; // 从末尾元素开始依次创建Node节点 for (int i = array.length - 1; i >= 0; i--) { Node node = new Node(array[i], null); // 创建两个节点的连接关系 if (root != null) { node.setNext(root); root = node; } else { root = node; } } // 返回根节点 return root; }
链表中索引内容读取
看一下数组是如何进行数据的读取的:
因为数组是连续内存空间存储的,通过
start_address + item_size * i
便能计算出索引地址,然后 1 步便完成内容的读取,时间复杂度是O(1)
那链表中应该如何操作呢?比如我们要找到 9, 2, 4
链表中索引为 2
的存储内容?
计算机肯定是无法一步完成这个操作,因为链表在内存中是分散存储的,我们现在只有第一个节点的地址,因此需要依次遍历找到第 3 个节点的地址
时间复杂度
从上图可以看出,链表中的读取时间复杂度和索引值有关。最好的情况是读取第一个节点,只需要一步。因此链表的读取时间复杂度为 O(N)
,相比较数组性能可能会差一点
链表中遍历查找元素
如果想要在链表 9, 2, 4
中查找是否包含某个节点它的内容是4,该如何操作?
我们同样需要利用第一个节点,依次往后面遍历,每次遍历到一个节点,比较这个节点内容是否为 4
逻辑很简单,所需步数和读取索引值一样,最好的情况在第一个节点就可以查找到,著需要1步。最差的情况是在最后一个节点才查找到,需要N步
所以时间复杂度为 O(N)
,查找元素的时间复杂度和数组是一样的
代码如下:
public class OrangeLinkedList { private Node root = null; int size = 0; public OrangeLinkedList() { } // 数组创建一个链表 public static Node createLinkedLNode(int[] array) { // 创建一个根节点 Node root = null; // 从末尾元素开始依次创建Node节点 for (int i = array.length - 1; i >= 0; i--) { Node node = new Node(array[i], null); // 创建两个节点的连接关系 if (root != null) { node.setNext(root); root = node; } else { root = node; } } return root; } public orangeLinkedList(int[] array) { this.root = YKDLinkedList.createLinkedLNode(array); this.size = array.length; } // 获取长度 public int size() { return this.size; } // 获取某个索引节点内容 public int get(int index) { int i = 0; Node node = this.root; // 依次往前推进 while (i < index) { node = node.getNext(); i++; } return node.getContent(); } // 查找某个值的索引值,默认不存在为-1 public int find(int value) { Node node = this.root; // index 保存当前的索引 int index = 0; // 依次遍历链表,找到内容等于value的node,返回index while (node != null) { if (node.getContent() == value) { return index; } node = node.getNext(); index++; } return -1; } // 提供数据进行测试 public static void main(String[] args) { int[] array = {1, 2, 3}; orangeLinkedList linkedList = new OrangeLinkedList(array); System.out.println(linkedList.find(2)); System.out.println(linkedList.find(8)); System.out.println(linkedList.get(2)); } }
头部插入分两部分执行:
- 新节点的 next 指向原来的 root 节点
- root 指针指向新节点
时间复杂度很容易计算出来为 O(1)
同理,中间插入也是分为两部分执行:
- 新节点 next 指向原来前置节点的 next
- 原来前置节点的 next,指向新节点
看起来也只需要 O(1)
就能完成插入,但是这样的插入有一个前置条件,也就是我们知道插入节点的前置节点。否则,我们只能根据索引通过 N步 找到前置节点
尾部插入和中间插入的逻辑完全一样。只是中间插入的时候前置节点的 next 是一个节点,而此处为 null:
- 新节点 next 指向原来前置节点的 next(此处为null)
- 原来前置节点的 next,指向新节点
通过上面的对比,我们可以总结如下:
如果已经知道插入节点的前置节点,那么链表的插入时间复杂度为
O(1)
普通情况(根据索引值插入节点),插入的时间复杂度为O(N)
这个和数组是一样的。但有趣的是,链表插入的最好和最差的情况刚好和数组相反,链表在头部插入很方便,但是数组开头插入确很麻烦。而数组在尾部插入很方便,但链表尾部插入要先扫描再插入
头部删除分为两步执行:
- root 指针指向第二个节点
- 原始 root 节点的 next 设置为 Null
时间复杂度很容易计算出来为 O(1)
同理,中间删除也分为两部分执行
- 待删除节点前置节点 next 指向待删除节点后置节点
- 待删除节点的 next,指向 Null
看起来好像也只需要 O(1)
就能完成删除,但是这样的删除有一个前置条件,也就是我们知道插入节点的前置节点。否则,我们只能根据索引通过 N步 找到前置节点
尾部删除和中间删除的逻辑完全一样。只是中间删除的时候前置节点的 next 改为待删除节点的后置节点,而此处后置节点为 null
待删除节点原本的 next 为 null,所以不需要再赋值为空。但是,我们仍然必须需要执行 N步 找到倒数第二个节点,再执行上面的删除操作
总结:其实删除操作,只是插入操作的反操作,原理还是比较容易理解,我们总结一下链表删除的时间复杂度
如果已经知道待删除节点的前置节点,那么链表的删除时间复杂度为
O(1)
普通情况(根据索引值删除节点),因为需要新找到前置节点,所以时间复杂度为O(N)
,这个和数组一样。和链表插入一样,链表删除的最好和最差的情况刚好和数组想反,链表在头部删除很方便,但是数组开头删除却很麻烦。而数组在尾部删除很方便,但链表尾部删除要先扫描再删除
链表插入和删除的代码如下:
public class OrangeLinkedList { private Node root = null; int size = 0; public OrangeLinkedList() { } // 数组创建一个链表 public static Node createLinkedLNode(int[] array) { // 创建一个根节点 Node root = null; // 从末尾元素开始依次创建Node节点 for (int i = array.length - 1; i >= 0; i--) { Node node = new Node(array[i], null); // 创建两个节点的连接关系 if (root != null) { node.setNext(root); root = node; } else { root = node; } } return root; } public OrangeLinkedList(int[] array) { this.root = YKDLinkedList.createLinkedLNode(array); this.size = array.length; } // 获取长度 public int size() { return this.size; } // 获取某个索引节点内容 public int get(int index) { int i = 0; Node node = this.root; // 依次往前推进 while (i < index) { node = node.getNext(); i++; } return node.getContent(); } // 查找某个值的索引值,默认不存在为-1 public int find(int value) { Node node = this.root; // index 保存当前的索引 int index = 0; // 依次遍历链表,找到内容等于value的node,返回index while (node != null) { if (node.getContent() == value) { return index; } node = node.getNext(); index++; } return -1; } // 末尾添加元素 public boolean add(int value) { return this.add(this.size - 1, value); } // 头部插入元素 public boolean addFirst(int value) { return this.add(-1, value); } // 插入元素 public boolean add(int index, int value) { if (index < -1 || index > this.size - 1){ return false; } if (index == -1) { if (this.root != null) { this.root = new Node(value, this.root); } else { this.root = new Node(value, null); } else { Node pre = this.root; while (index > 0) { if (pre.getNext() == null) { return false; } pre = pre.getNext(); index--; } if (pre == null) { return false; } Node newNode = new Node(value, pre.getNext()); pre.setNext(newNode); } this.size++; return true; } // 删除最后一个元素 public boolean removeLast() { return this.remove(this.size - 1); } // 删除第一个元素 public boolean removeFirst() { return this.remove(0); } // 插入元素 // index = 0, 表示删除第 1 个元素 // index = n,表示删除第 n+1 个元素 public boolean remove(int index) { if (index < 0 || index > this.size - 1){ return false; } // 判断整个列表为空情况,删除失败 if (this.root == null) { return false; } if (index == 0) { // 删除第一个元素,情况比较简单,注意必须先保留 this.root,因为紧接着 this.root 会被修改 Node node = this.root; this.root = node.getNext(); node.setNext(null); } else { // 判断越界问题 if(this.size < index + 1){ return false; } // 遍历获取index的前置节点 Node pre = this.root; while (index > 1) { pre = pre.getNext(); index--; } // 修改next指针,同样大家需要先保存 pre.next,因为紧接着下一步会修改 pre.next 指针 Node next = pre.getNext(); pre.setNext(next.getNext()); next.setNext(null); } this.size--; return true; } // 链表中的元素字符串形式 public String toString() { if (this.root == null) { return ""; } StringBuilder str = new StringBuilder(); Node node = this.root; while (node != null) { str.append(node.getContent()).append(" "); node = node.getNext(); } return str.toString(); } public static void main(String[] args) { int[] array = {10, 9, 2, 4}; OrangeLinkedList linkedList = new OrangeLinkedList(array); boolean result = linkedList.removeFirst(); if (!result) { System.out.println("removeFirst失败"); return; } result = linkedList.removeLast(); if (!result) { System.out.println("removeLast失败"); return; } result = linkedList.remove(1); if (!result) { System.out.println("remove(1)失败"); return; } System.out.println(linkedList.toString()); } }
链表与数组在性能上有什么区别?
数组 | 链表 | |
---|---|---|
存储 | 连续内存存储 | 分散a内存存储 |
读取 | O(1) | O(N) |
插入 | O(N) | O(N) |
插入最好情况 | 数组末尾插入 | 链表开头插入 |
插入最坏情况 | 数组开头插入 | 链表末尾插入 |
删除 | O(N) | O(N) |
删除最好情况 | 数组末尾删除 | 链表开头删除 |
删除最坏情况 | 数组开头删除 | 链表末尾删除 |
链表看似除了在存储方面有优势,好像其他都不如数组。但是在复杂场景下,表现并不是这样:
我们手里有一堆手机号码,我们希望写一个算法删除无效的手机号码。具体逻辑为:我们每次读取一个手机号码,判断这个手机号码是否为11为,并且满足一定的正则表达式要求
这种场景下应该用什么数据结构存储这一堆手机号码呢?
在这个场景下最关键的两个因素遍历和删除
遍历
无论使用数组或者链表进行存储,都需要经历 N 次遍历。这部分无法减小
删除
如果使用数组删除的话,每次都需要移动后面的元素,所以每次删除的时间复杂度都是 O(N)
而如果使用链表的话,就不存在这个问题。因为在遍历场景下,我们都可以拿到需要删除节点的前置节点,所以在这个场景下的删除时间复杂度为 O(1)
总结
简单统计下,在这种场景下使用数组的操作的时间复杂度为 O(N^2)
。而使用链表的时间复杂度为 O(N)
从这个例子看出链表在频繁插入和删除的情况下,有绝对的优势!
双向链表的特点:
- 对于每个节点,由三部分组成,
prev
(指向前一个节点),next
(指向后一个节点),content
(节点内容)- 对于双向链表,为了方便遍历,我们需要同时保留第一个节点和最后一个节点
阅读 LinkedList
源码,我们会发现它实际上就是利用 双向链表 实现的,在源码中我们可以找到如下代码:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { transient int size = 0; /** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; /** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last; /** * Constructs an empty list. */ public LinkedList() { } ...... }
需要关注这里的两个变量 first
和 last
。分别表示双向链表的头部和尾部两个节点
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。