本章我们介绍链表

前面我们已经介绍了动态数组,栈和队列。

它们的底层依托静态数组;靠resize解决固定容量问题

链表是我们接触的第一个真正的动态数组。

为什么链表很重要

链表是重点,也是难点。它是最简单动态数据结构;后续我们还会学习更多的,比如二分搜索树,平衡二叉树,红黑树,后面很多的动态数据结构都可以在理解链表的基础上学习。

链表可以让我们更深入的理解引用(C++中指针),内存管理等有更深理解。对于更深入的理解递归有好处,树形中递归必须理解。

链表可以辅助组成其他数据结构。

链表Linked List

数据存储在“节点”(Node)中;

class Node{E e;Node next;
}

车厢和车厢进行连接,使用next进行连接。

最后一个节点的next指向空,说明这个节点是最后一个节点了。优点:真正的动态,不需要处理固定容量的问题

不像数组一下子必须new出来一片空间,需要考虑空间不够用或浪费。链表是你需要多少个数据,就生成多少个节点将他挂接起来,这就是所谓的动态的意思。

缺点: 丧失了随机访问的能力。不能像数组一样,给定一个索引直接拿出对应元素。底层机制中数组开辟的空间在内存中是连续分布的,我们可以直接寻找索引对应的偏移,直接计算出数据所存储的内存地址,直接用O(1)复杂度拿出。链表靠next连接,每个节点存储地址不同,我们只能通过next顺藤摸瓜找到我们要找的元素。

数组最好用于索引有语意的情况。scores[2] 2是学号,身份证号不能做索引;最大的优点:支持快速查询。

我们在编写动态数组,但是其实这类索引没有语义的情况更适合链表。

链表不适合用于索引有语意的情况。最大的优点:动态

什么时候适合使用数组,什么时候适合使用链表。

链表实现

package cn.mtianyan;public class LinkedList<E> {// private设计,不被用户感知private class Node{public E e;public Node next; // c++实现时是指针public Node(E e, Node next) {this.e = e;this.next = next;}public Node(E e) {this.e = e;this.next = null;}public Node() {this(null,null);}@Overridepublic String toString() {return "Node[" +"e=" + e +", next=" + next +']';}}
}

上面是我们对于链表节点的设计。注意private设计,以及Node的成员变量Node

应该有一个链表头,声明出LinkedList基本的成员变量。

 private Node head;private int size;public LinkedList() {head = null;size = 0;}public LinkedList(Node head, int size) {this.head = head;this.size = size;}/*** 从数组创建链表的方法,待完善。** @param e*/public LinkedList(E[] e){}/*** 获取链表中元素个数** @return*/public int getSize(){return size;}/*** 返回链表是否为空** @return*/public boolean isEmpty(){return size == 0;}

上面是链表中应该有的成员变量和一些普通方法。

在链表头添加元素是非常方便的,数组在数组尾部添加元素不用挪位会非常方便。数组中有size指向下一个空位置跟踪队尾,链表中有head来标识链表的头部。

node.next = head
head = node
    public void addFirst(E e){//        Node node = new Node(e);//        node.next = head;//        head = node;// 上面三行代码的等价实现head = new Node(e,head); // 值为e的Node的next是head;head = 这个Nodesize++;}

上面有两种等价的实现。

在索引为2的地方添加元素666,要找到之前的节点。关键:找到要添加的节点的前一个节点。前一个节点要特殊处理

顺序是很重要的,不能颠倒。否则会丢失原本的prev.next。大多时候顺序可以省下一个old的备份临时变量。

    /*** 在链表的index(0-based)位置添加新的元素e* 在链表中不是一个常用的操作,练习题用,面试用。* @param index* @param e*/public void add(int index,E e){// index可以取到size,在链表末尾空位置添加元素。if (index < 0 || index >size){throw new IllegalArgumentException("Add failed. Illegal index");}Node prevNode = head;// 因为有了dummyHead,多遍历一次,遍历index次for (int i = 0; i < index-1; i++) {// 验证。 12 index 1添加,index-1=0一次也不执行,正好是head。符合// 验证。 1234 index 2添加,index-1=1 运行一次pre指向head下一个也就是2,符合。prevNode = prevNode.next;}//        Node insertNode = new Node(e);//        insertNode.next = prevNode.next;//        prevNode.next = insertNode;prevNode.next = new Node(e,prevNode.next); // 后半截是前两句完成任务size++;}

链表的添加操作时,要找的是前一个节点。而我们之前定义的头结点因为没有前一个节点,需要进行特殊处理,这样不够优雅。而如果我们往前面加一个虚拟的头结点,则可以将我们现在的头结点和其他节点统一起来。

    private Node dummyHead;public LinkedList() {dummyHead = new  Node(null,null);size = 0;}

虚拟头结点对用户屏蔽不可见。

    /*** 在链表的index(0-based)位置添加新的元素e* 在链表中不是一个常用的操作,练习题用,面试用。* @param index* @param e*/public void add(int index,E e){// index可以取到size,在链表末尾空位置添加元素。if (index < 0 || index >size){throw new IllegalArgumentException("Add failed. Illegal index");}Node prevNode = dummyHead;// 因为有了dummyHead,多遍历一次,遍历index次for (int i = 0; i < index; i++) {// 验证。 12 index 1添加,index-1=0一次也不执行,正好是head。符合// 验证。 1234 index 2添加,index-1=1 运行一次pre指向head下一个也就是2,符合。prevNode = prevNode.next;}//        Node insertNode = new Node(e);//        insertNode.next = prevNode.next;//        prevNode.next = insertNode;prevNode.next = new Node(e,prevNode.next); // 后半截是前两句完成任务size++;}/*** 在链表头添加新元素e*/public void addFirst(E e){add(0,e);}/***  在链表末尾添加新的元素e*/public void addLast(E e){add(size,e);}

添加元素操作时,注意指向,以及循环次数的验证。

/***  获得链表的第index(0-based)位置元素*  链表中不是常用操作,练习用* @param index* @return*/public E get(int index){// index不可以取到size,索引从0开始,最多取到size-1if (index < 0 || index >=size){throw new IllegalArgumentException("Add failed. Illegal index");}Node cur = dummyHead.next; // 从索引为0元素开始// 下面与找index-1个节点保持一致。上面执行了一次。所以从index-1个元素变成了找index个元素。for (int i = 0; i < index; i++) {cur = cur.next;}return cur.e;}public E getFirst(){return get(0);}public E getLast(){return get(size-1);}

插入时我们要寻找的是index的前一个位置,而get时,我们要找的就是index的当前位置,因此要多找一次,在for循环不变情况下,从虚拟头结点下一个节点开始遍历。

 /*** 修改链表的第index(0-based)个位置的元素为e* 在链表中不是一个常用的操作,练习用*/public void set(int index,E e){// index不可以取到size,索引从0开始,最多取到size-1if (index < 0 || index >=size){throw new IllegalArgumentException("Set failed. Illegal index");}Node cur = dummyHead.next; // 从索引为0元素开始// 下面与找index-1个节点保持一致。上面执行了一次。所以从index-1个元素变成了找index个元素。for (int i = 0; i < index; i++) {cur = cur.next;}cur.e = e;}/***     查找链表中是否有元素e*/public boolean contains(E e){Node cur = dummyHead.next;while (cur != null){if (cur.e.equals(e)){return true;}cur = cur.next;}return false;}
 @Overridepublic String toString() {StringBuilder res = new StringBuilder();
//        Node cur = dummyHead.next;
//        while (cur != null){
//            res.append(cur.e +"->");
//            cur = cur.next;
//        }
//        res.append("NULL");res.append("head: ");for (Node cur=dummyHead.next;cur !=null;cur=cur.next){res.append(cur.e +"->");}res.append("NULL");return res.toString();}

两种不同的遍历方式是等价的。

package cn.mtianyan;public class Main {public static void main(String[] args) {LinkedList<Integer> linkedList = new LinkedList<>();for (int i = 0; i < 5; i++) {linkedList.addFirst(i);System.out.println(linkedList);}linkedList.add(2,888);System.out.println(linkedList);}
}

运行结果:

删除元素

删除索引为2位置的元素

要找到它之前的元素。

prev.next = delNode.next
delNode.next = null
  • 链表元素删除时常见的错误。

cur 指向cur.next的位置。本质是对于引用概念糊涂,Java中类的对象都是一个引用,理解成一个实际内存的指向。cur = cur.next从原来指的位置,指到下一个位置,但对于链表来说没有发生任何改变。要想改变链表就应该改变节点的next指向。

    /*** 删除链表中指定index位置的元素* @param index* @return*/public E remove(int index){if (index < 0 || index >=size){throw new IllegalArgumentException("Set failed. Illegal index");}Node prev = dummyHead;for (int i = 0; i < index; i++) {prev = prev.next;}Node retNode = prev.next;prev.next = retNode.next;retNode.next = null;size--;return retNode.e;}public E removeFirst(){return remove(0);}public E removeLast(){return remove(size-1);}
        linkedList.remove(2);System.out.println(linkedList);linkedList.removeFirst();System.out.println(linkedList);linkedList.removeLast();System.out.println(linkedList);

运行结果:

链表时间复杂度分析

  • 添加操作:

O(n)是因为往链表尾部添加,要遍历整个链表节点。O(n/2)可以看做操作中间的节点。

  • 删除操作:
  • 修改操作:
set(index e)  // O(n/2) = O(n)
  • 查找操作:

get 和 contains 都是O(n/2) find操作是根据元素找index,链表中index没啥用。

看起来,链表的增删改查全都是O(n)级别的,比数组看起来差。链表没有索引,无法像数组一样快速访问。

此时我们能利用的方法复杂度都是O(1)了;链表的改进,比数组节省空间。最基础动态数据结构,对二叉树平衡二叉树的学习都能有辅助作用。

链表实现栈

只对链表头进行操作,也就是只能对一端进行操作,很明显是栈。队列是要对两端都进行操作的。链表头作为栈顶。

    Interface Stack<E> implement LinkedListStack<E>int getSize();boolean isEmpty();void push(E e);E pop();E peek();

比较两个栈的性能差异。

package cn.mtianyan;public class LinkedListStack<E> implements Stack<E> {private LinkedList<E> list;public LinkedListStack() {list = new LinkedList<>();}@Overridepublic int getSize() {return list.getSize();}@Overridepublic boolean isEmpty() {return list.isEmpty();}@Overridepublic void push(E e) {list.addFirst(e);}@Overridepublic E pop() {return list.removeFirst();}@Overridepublic E peek() {return list.getFirst();}@Overridepublic String toString() {StringBuilder res = new StringBuilder();res.append("LinkedList Stack :");res.append(list);return  res.toString();}
}
    public static void main(String[] args) {LinkedListStack stack = new LinkedListStack();for (int i = 0; i < 5; i++) {stack.push(i);System.out.println(stack);}stack.pop();System.out.println(stack);}

运行结果:

package cn.mtianyan;import java.util.Random;public class mainTwoTest {// 测试使用stack运行opCount个push和pop操作所需要的时间,单位:秒private static double testStack(Stack<Integer> stack, int opCount){long startTime = System.nanoTime();Random random = new Random();for(int i = 0 ; i < opCount ; i ++)stack.push(random.nextInt(Integer.MAX_VALUE));for(int i = 0 ; i < opCount ; i ++)stack.pop();long endTime = System.nanoTime();return (endTime - startTime) / 1e9;}public static void main(String[] args) {int opCount = 100000000;ArrayStack<Integer> arrayStack = new ArrayStack<>();double time1 = testStack(arrayStack, opCount);System.out.println("ArrayStack, time: " + time1 + " s");LinkedListStack<Integer> linkedListStack = new LinkedListStack<>();double time2 = testStack(linkedListStack, opCount);System.out.println("LinkedListStack, time: " + time2 + " s");// 其实这个时间比较很复杂,因为LinkedListStack中包含更多的new操作}
}

其实这个时间是比较不确定谁大谁小的。

运行结果:

100000000 数据:

10000000 数据:

1000000 数据:

100000 数据:

基本可以看出,数据量小于100万的时候LinkedList比较有优势,数据量大时ArrayList更优。但它们实际是同样级别时间复杂度的,最多相差几倍。

链表实现队列

队列势必会在链表的两端同时操作,一端为O(1)一端为O(n);使用数组时我们也遇到了这个问题,因此我们产生了使用循环队列的方式。

链表中我们为什么对于链表头部的操作都简单一些呢,因为我们有一个标识的head。那么想让尾部也可以操作简单,设置一个tail变量。从两端插入元素都是很容易的。

tail端前一个节点不容易找,得遍历一遍。此时: head添加删除都容易,tail添加容易,删除不易。

因此队列从head端删除元素,从tail端插入元素。head 队首负责出队,tail队尾负责入队。由于没有dummyHead,要注意链表为空的情况

package cn.mtianyan;public class LinkedListQueue<E> implements Queue<E> {private class Node{public E e;public Node next;public Node(E e, Node next){this.e = e;this.next = next;}public Node(E e){this(e, null);}public Node(){this(null, null);}@Overridepublic String toString(){return e.toString();}}private Node head, tail;private int size;public LinkedListQueue(){head = null;tail = null;size = 0;}@Overridepublic int getSize(){return size;}@Overridepublic boolean isEmpty(){return size == 0;}@Overridepublic void enqueue(E e){// 如果队尾为空,说明队列是空的。因为tail一直指向最后一个非空节点。if(tail == null){tail = new Node(e);head = tail;}else{// 使用tail.next把新Node挂载上来。tail.next = new Node(e);// tail后挪tail = tail.next;}size ++;}@Overridepublic E dequeue(){if(isEmpty())throw new IllegalArgumentException("Cannot dequeue from an empty queue.");Node retNode = head;head = head.next; // head后移retNode.next = null; // 元素置空if(head == null) // 如果头结点都没得删了tail = null;size --;return retNode.e;}@Overridepublic E getFront(){if(isEmpty())throw new IllegalArgumentException("Queue is empty.");return head.e;}@Overridepublic String toString(){StringBuilder res = new StringBuilder();res.append("Queue: front ");Node cur = head;while(cur != null) {res.append(cur + "->");cur = cur.next;}res.append("NULL tail");return res.toString();}public static void main(String[] args){LinkedListQueue<Integer> queue = new LinkedListQueue<>();for(int i = 0 ; i < 5 ; i ++){queue.enqueue(i);System.out.println(queue);if(i % 3 == 2){queue.dequeue();System.out.println(queue);}}}
}

运行结果:

测试性能差异:

package cn.mtianyan;import java.util.Random;public class MainThree {// 测试使用q运行opCount个enqueueu和dequeue操作所需要的时间,单位:秒private static double testQueue(Queue<Integer> q, int opCount){long startTime = System.nanoTime();Random random = new Random();for(int i = 0 ; i < opCount ; i ++)q.enqueue(random.nextInt(Integer.MAX_VALUE));for(int i = 0 ; i < opCount ; i ++)q.dequeue();long endTime = System.nanoTime();return (endTime - startTime) / 1000000000.0;}public static void main(String[] args) {int opCount = 100000;ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();double time1 = testQueue(arrayQueue, opCount);System.out.println("ArrayQueue, time: " + time1 + " s");LoopQueue<Integer> loopQueue = new LoopQueue<>();double time2 = testQueue(loopQueue, opCount);System.out.println("LoopQueue, time: " + time2 + " s");LinkedListQueue<Integer> linkedListQueue = new LinkedListQueue<>();double time3 = testQueue(linkedListQueue, opCount);System.out.println("LinkedListQueue, time: " + time3 + " s");}
}

运行结果:

链表是一种适合用来学习递归的数据结构。下一章我们将对于链表和递归的相关知识进行学习。

4-玩转数据结构-链表相关推荐

  1. 初学数据结构--链表

    2019独角兽企业重金招聘Python工程师标准>>> 前言 在这一章,我将介绍另外一种非常重要的线性数据结构--链表.在之前介绍的动态数组,栈和队列这三种数据结构,底层其实依托于静 ...

  2. C++数据结构链表的基本操作

    这篇文章主要为大家介绍了C++数据结构链表基本操作的示例过程有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪 首先创建好一个节点 typedef struct node {in ...

  3. c语言仓库管理系统链表,仓库管理系统 C语言 C++ 数据结构 链表 课程设计

    仓库管理系统 C语言 C++ 数据结构 链表 课程设计 #include #include #include #include #define MAX 64 typedef struct node{ ...

  4. 数据结构 - 链表 - 面试中常见的链表算法题

    数据结构 - 链表 - 面试中常见的链表算法题 数据结构是面试中必定考查的知识点,面试者需要掌握几种经典的数据结构:线性表(数组.链表).栈与队列.树(二叉树.二叉查找树.平衡二叉树.红黑树).图. ...

  5. 数据结构链表之符号表,Python3实现——8

    数据结构链表之符号表 符号表的介绍 之前章节介绍的顺序表和链表都是一个节点储存一个元素的表,但在日常生活中我们还有很多一次需要储存成对或多个值的情况,例如: 符号表最主要的目的将一对元素,用一个键和一 ...

  6. 数据结构链表之队列,Python3实现——7

    数据结构链表之队列 队列概述 定义:队列是一种基于先进先出(FIFO)的数据结构,队列只能在一段进行插入和删除操作的结构,第一个进入队列的元素在读取时会第一个被读取 队列可以使用顺序表(Python中 ...

  7. 数据结构链表之栈,Python3简单实现——5

    数据结构链表之栈 栈的概述 定义:栈是一种基于先进后出(FILO)的数据结构,是一种只能在一段进行插入和删除操作的特殊线性表. 引入名词:将数据存入栈的动作称为压栈,将数据取出栈的动作称为弹栈 栈的特 ...

  8. 数据结构链表例程_如何掌握RxJava例程的四个结构

    数据结构链表例程 by Ayusch Jain 通过Ayusch Jain 如何掌握RxJava例程的四个结构 (How to get a grip on the four constructs of ...

  9. 数据结构链表代码_代码简介:链表数据结构如何工作

    数据结构链表代码 Here are three stories we published this week that are worth your time: 这是我们本周发布的三个值得您关注的故事 ...

  10. python创建链表实例_python数据结构链表之单向链表(实例讲解)

    python数据结构链表之单向链表(实例讲解) 单向链表 单向链表也叫单链表,是链表中最简单的一种形式,它的每个节点包含两个域,一个信息域(元素域)和一个链接域.这个链接指向链表中的下一个节点,而最后 ...

最新文章

  1. 《火星救援VR》原班人马打造全新AR游戏,让可爱小飞龙伴随你左右
  2. Keycloak简单几步实现对Spring Boot应用的权限控制
  3. BZOJ1688|二进制枚举子集| 状态压缩DP
  4. 鸿蒙比不了IOS,比苹果的iOS14还流畅,华为鸿蒙2.0可以啊!
  5. 信息学奥赛一本通 1939:【07NOIP普及组】纪念品分组 | P1094 [NOIP2007 普及组] 纪念品分组
  6. python之Django学习笔记(五)---后台(admin.py)Action使用
  7. mysql系统变量配置文件_MySQL系统变量配置基础
  8. MVVM模式与Reactive Extensions 学习与思考
  9. 被窃听、被定位:“裸奔时代”还有隐私吗?
  10. Linux防火墙-netfilter-iptables
  11. sql server 查询当前月份日期列表数据
  12. html制作dnf,dnf怎么制作img文件 时装拼合教程
  13. EMC标准与测试方法
  14. APP兼容性测试如何测试?
  15. c#万能视频播放器(附代码)
  16. Spring Cloud微服务实战_PDF电子书下载 高清 带索引书签目录_翟永超(著)
  17. JavaScript刷新和跳转
  18. 华东理工大学本科毕业论文答辩和论文选题PPT模板
  19. 微信小程序 满意度调查问卷
  20. 引爆点 mobi_开源中的3个新兴引爆点

热门文章

  1. [Django] 查看orm自己主动运行的原始查询sql
  2. MySQL自增长主键探究
  3. 《构建之法》第三周阅读笔记
  4. Objective-C中把URL请求的参数转换为字典
  5. java tomcat mysql_java+tomcat+mysql实现登录界面基本思路
  6. Windows PE第6章 栈与重定位表
  7. 【错误记录】Android Gradle 配置报错 ( gradle.properties 配置到 BuildConfig 中需要注意类型转换 | 位置: 类 BuildConfig )
  8. 【C 语言】文件操作 ( 文件加密解密 | 加密解密原理 | 对称加密 | 非对称加密 | 散列函数 )
  9. 【Android 逆向】Android 系统文件分析 ( /system/ 系统命令和系统应用数据目录 | /system/app/ 系统应用目录 | sys Linux 系统内核文件目录 )
  10. 【EventBus】EventBus 源码解析 ( EventBus 构建 | EventBus 单例获取 | EventBus 构造函数 | EventBus 构建者 )