数据结构之链表 - 动图演示
微信搜索【NO编程】,关注这个与众不同的公众号。
个人网站:www.newobject.cc
版权声明:本文为原创文章,转载请注明出处。
链表简介
链表是很常见的数据结构,由一个个节点组成,每个节点中储存着数据和指针(地址引用),指针负责节点间的连接。
它是一种线性表,线性表有两种存储方式:顺序存储和链式存储。链表属于链式存储,顺序由元素间的指针决定,元素在内存中非连续存放,且链表长度可以改变。数组是顺序存储的线性表,元素在内存中连续存放的,且数组创建时大小已固定。
链表可以用来实现栈和队列数据结构(栈和队列可理解为逻辑类数据结构,链表属于存储类数据结构),实现缓存 LRU 算法,Java 类库也使用了链表(如,LinkedList,LinkedHashMap)等。链表的形式有很多,常用的有单向链表、双向链表、循环链表 …
单向链表
单链表中的节点分两部分,分别是数据(data)和指向下一个节点的地址(next),尾节点(tail)的 next 指向 null。单向链表只能从头到尾一个方向遍历,查找节点时需要从头节点(head)开始向下查找。
插入节点首先遍历查找到插入的位置,然后将当前插入节点的 next 指向下一节点,上一节点的 next 指向当前插入节点。删除节点同样从头遍历找到要删除的节点,然后将当前删除节点的上一个节点 next 指向当前删除节点的下一个节点。
节点的伪代码:
class Node<E>{private E item;private Node<E> next; // 如果是尾节点,next 指向 nullNode(E data, Node<E> next) {this.item = data;this.next = next;}// ...
}
单向循环链表
循环链表和非循环链表基本一样,区别是首尾节点连在了一起,最后一个节点的 next 指向头节点,形成了一个闭环。
节点的伪代码:
class Node<E>{private E item;// 如果是末尾节点,指向首节点的引用地址private Node<E> next;Node(E data, Node<E> next) {this.item = data;this.next = next;}// ...
}
双向链表
顾名思义,与单向链表相比较,双向链表可以从头到尾或从尾到头两个方向来遍历数据。双向链表中的节点分三个部分,分别是指向上一个节点的地址(prev)和数据(data)以及指向下一个节点的地址(next),尾节点(tail)节点的 next 指向 null,头节点(head)的 prev 指向 null。
增加和删除节点和单向链表同理,只是增加了修改 prev 地址的操作。
节点的伪代码:
class Node<E>{private E item;private Node<E> prev; // 头节点 prev 指向 nullprivate Node<E> next; // 尾节点 next 指向 nullNode(Node<E> prev, E data, Node<E> next) {this.item = data;this.prev = prev;this.next = next;}// ...
}
双向循环链表
尾节点的next指向头节点,头节点的 prev 指向尾节点,首尾节点连在一起形成闭环。
节点的伪代码:
class Node<E> { private E item;// 如果是第一个节点,其一用指向末尾节点private Node<E> prev;// 如果是末尾节点,指向第一个节点的引用地址,形成一个环形private Node<E> next;Node(Node<E> prev, E data, Node<E> next) {this.item = data;this.prev = prev;this.next = next;}// ...
}
链表操作
链表的增删改查操作。链表查找节点需要从头或者尾部(单向链表只能从头开始)开始查找,删除或插入节点先查找到节点,然后改变相关节点的指针指向即可。
以双向链表为例:
添加节点
- 头部添加节点
伪代码:
Node<E> head;
Node<E> tail;
int size;// 头部添加节点
void addHead(E e) {Node<E> h = head;Node<E> newNode = new Node<>(null, e, h); // (Node<E> prev, E element, Node<E> next)head = newNode;if(h == null) { // 空链表tail = newNode;} else {h.prev = newNode;} size++; // 记录长度
}
- 尾部添加节点
伪代码:
void addTail(E e) {Node<E> t = tail;Node<E> newNode = new Node<>(t, e, null);tail = newNode;if(t == null) {head = newNode;} else {t.next = newNode; } size++;
}
- 按位置插入节点
伪代码:
void add(int index, E element) {if (index == size) {// 直接在尾部添加节点} else {// 查找的节点Node<E> temp = null;if (index < (size >> 1)) {//由于双向链表,选择从离 index 位置最近端查找Node<E> x = head;for (int i = 0; i < index; i++) {x = x.next;}temp = x; } else {Node<E> x = tail;for (int i = size - 1; i > index; i--) {x = x.prev;}temp = x; }// 插入节点Node<E> pred = temp.prev;Node<E> newNode = new Node<>(pred, element, temp);temp.prev = newNode;if (pred == null) { // 查找到的节点为 Head 节点head = newNode;} else {pred.next = newNode;} } size++;}
删除节点
- 删除头部节点
伪代码:
E removeHead() {Node<E> h = head; if (h != null){E element = h.item;Node<E> next = h.next;head = next;if (next == null) {tail = null;} else {next.prev = null;} size--; // 减少长度return element; // 返回删除元素}return null;
}
- 删除尾部节点
伪代码:
E removeTail() {Node<E> t = tail;if (t != null) {E element = t.item;Node<E> prev = t.prev;tail = prev;if (prev == null) {head = null;} else {prev.next = null;}size--;return element;}return null;
}
- 按节点位置或值删除
伪代码-按位置删除:
E remove(int index) {// 根据index查找节点Node<E> temp = null;if (index < (size >> 1)) {Node<E> x = head;for (int i = 0; i < index; i++) {x = x.next;}temp = x; } else {Node<E> x = tail;for (int i = size - 1; i > index; i--) {x = x.prev;}temp = x; }// 删除节点E element = temp.item;Node<E> next = temp.next;Node<E> prev = temp.prev;if (prev == null) {head = next;} else {prev.next = next;temp.prev = null;}if (next == null) {tail= prev;} else {next.prev = prev;temp.next = null;}temp.item = null;size--; return element;
}
查找节点
- 按位置或值查找节点
伪代码-按位置索引查找:
E get(int index) {Node<E> temp = null;if (index < (size >> 1)) { // 从近的一端开始查找Node<E> x = first;for (int i = 0; i < index; i++) {x = x.next;} temp = x;} else {Node<E> x = last;for (int i = size - 1; i > index; i--) {x = x.prev;} temp = x;}return temp.item;
}
如果是单向链表只能从头部开始向后查找。
更新节点
更新节点首先查找到节点,然后修改节点 data 的指针。
具体可参考 LinkedList 源码
链表实现栈和队列
栈和队列是一种对数据存取有严格顺序要求的线性数据结构,使用链表和数组都能实现。下面使用链表来实现栈和队列。
栈
栈只能从一端存取数据,遵循 后进先出(LIFO) 原则。进出栈的一端称为栈顶,另一封闭端称为栈底,数据进入栈称为入栈或压栈,取出数据称为出栈或弹栈。
伪代码 - 基于双向链表实现简单的“栈”:
class Stack<E> {// 返回栈顶元素值public E peek() {Node<E> h = head;return (h == null) ? null : h.item;}// 入栈public void push(E e) {addHead(e); // 在头部添加节点}// 出栈public E pop() {// 移除头部节点并返回值return removeHead(); }// ...private static class Node<E> {E item;Node<E> next;Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}}
}
队列
队列是从两端存取数据,并且从一端进,从另一端出,遵循 先进先出(FIFO) 原则。队列进数据一端称为队尾,出数据端称为队头,数据进队列称为入队,取出队列称为出队。
伪代码 - 基于链表实现“队列”:
class Queue {// 入队public boolean offer(E e) {return addTail(e); }// 出队public E poll() {return removeHead();}// 返回头元素值public E peek() {Node<E> h = head;return (h == null) ? null : h.item;}private static class Node<E> {E item;Node<E> next;Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}}
}
快慢指针
快慢指针是解决链表某些问题的常用方法,利用两个不同步频的指针 fast 指针和 slow 指针算法来解决很多问题,例如:
查找未知长度的单向链表倒数第N个值
由于链表长度未知,首先循环链表得到 length,然后再次循环链表到 length-(N-1) 处得到元素。但是利用快慢指针来保持固定位置间隔,只需要循环一次链表即可查找到元素。
伪代码:
public E getLastN(int n) {Node<E> h = head;if (h == null || n < 1) {return null;}Node<E> fast = h; // 快Node<E> slow = h; // 慢int count = 1;while ((fast = fast.next) != null) {// 倒数第 k 个节点与倒数第 1 个节点相隔 n-1 个位置,因此 fast 先走 n-1 个位置if (count++ > n - 1) {slow = slow.next;}}// 链表中的元素个数小于 nif (count < n) {return null;}return slow.item;
}
找到链表中间节点值
使快指针移动步频是慢指针二倍,一次遍历即可快速找到中间节点。
伪代码:
public E getMiddle() {Node<E> h = head;if (h == null) {return null;}Node<E> fast = h; // 快Node<E> slow = h; // 慢while (fast != null && fast.next != null) {fast = fast.next.next;// 链表长度为偶数会两个中间节点,返回第一个if (fast != null) {slow = slow.next;}}return slow.item;
}
源码:https://github.com/newobjectcc/code-example/blob/master/basic/datastructure/Linked.java
除此之外,还可以判断链表中是否有环等等问题,快慢指针在面试时可能会被问到,有兴趣朋友可以到网上找些链表的算法题。
原创不易,如果本文对你有帮助,烦请朋友们,点赞、在看或转发支持一下, 这会让我更有创作的动力。
微信搜索「NO编程」或扫描下方二维码关注我的公众号,该公众号致力于通过文字和动图来讲解 Java 相关知识,一起学习交流,共同进步成长,欢迎关注。
数据结构之链表 - 动图演示相关推荐
- 数据结构与算法--经典10大排序算法(动图演示)【建议收藏】
十大经典排序算法总结(动图演示) 算法分类 十大常见排序算法可分为两大类: 比较排序算法:通过比较来决定元素的位置,由于时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序 非比较类型排 ...
- c 语言从大到小排序算法,10 大经典排序算法(动图演示+ C 语言代码)
原标题:10 大经典排序算法(动图演示+ C 语言代码) 来源:C语言与CPP编程 以前也零零碎碎发过一些排序算法,但排版都不太好,又重新整理一次,排序算法是数据结构的重要部分,系统地学习很有必要. ...
- 一文总结十大经典排序算法(思维导图 + 动图演示 + 代码实现 C/C++/Python + 致命吐槽)
声明 1)该文章整理自网上的大牛和专家无私奉献的资料,具体引用的资料请看参考文献. 2)本文仅供学术交流,非商用.如果某部分不小心侵犯了大家的利益,还望海涵,并联系博主删除. 3)博主才疏学浅,文中如 ...
- 算法 - 十大经典排序算法(动图演示)
[TOC] 算法 - 十大经典排序算法(动图演示) 在计算机科学与数学中,一个排序算法(英语:Sorting algorithm)是一种能将一串资料依照特定排序方式进行排列的一种算法.最常用到的排 ...
- 十大经典排序算法Python版实现(附动图演示)
来源:大数据DT 本文约5200字,建议阅读10分钟 排序算法是<数据结构与算法>中最基本的算法之一.本文介绍10种常见的内部排序算法,及如何用Python实现. 排序算法可以分为内部排序 ...
- 一文读懂Python版的十大经典排序算法(附动图演示)
来源:大数据DT 本文约5200字,建议阅读10分钟 排序算法是<数据结构与算法>中最基本的算法之一.本文介绍10种常见的内部排序算法,及如何用Python实现. 排序算法可以分为内部排序 ...
- 十大经典排序算法(动图演示,收藏好文)
0.算法概述 0.1 算法分类 十种常见排序算法可以分为两大类: 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序. 线 ...
- 十大经典排序C++实现及动图演示
0.算法概述 0.1 算法分类 十种常见排序算法可以分为两大类: 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序. 非比较类排序: ...
- 程序员面试必备:动图演示十大经典排序算法及代码实现
0.算法概述 0.1 算法分类 十种常见排序算法可以分为两大类: 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序. 非比较类排序: ...
最新文章
- mvc的宿舍管理系统源码 基于jsp_[源码和文档分享]基于JSP的MVC框架实现的图书推荐系统展示平台网站...
- 阿里开源混沌工程工具 ChaosBlade
- jquery 对 Json 的各种遍历
- chk mysql.sh_zabbix监控mysql_MySQL
- Java程序优化的一些最佳实践
- 喵哈哈村与哗啦啦村的大战(四)(树形DP)
- R学习-小白笔记08
- stella forum 知识库---一些错误的修补
- VS2017编写C++多文件时,出现LNK2005、LNK1169报错的解决方法
- ant design pro模板_ant design pro超详细入门教程
- JDK1.8中英文官方文档
- MySQL8.0密码找回与权限刷新
- css3中边框的4种样式
- 全国计算机考试分几个等级?怎么报考呢?
- 2022 最值得学习的编程语言:Python 高人气,Ruby 薪水最优渥
- Intel8086处理器使用NASM汇编语言实现操作系统15-段的定义section/vstart和align语法
- 自定义带取景框的camera
- python 使用 openpyxl 批量调整字体和样式
- ADATE320介绍
- Hive分析函数之SUM,AVG,MIN和MAX OVER(PARTITION BY xxx order by xxx,用于求一段时间内截至到每天的累计访问次数、平均访问次数、最小访问次数、最大访问次