链表以head 为头节点,head中不存放任何数据,代表链表的开始
链表也是一种线性表,但是链表中的节点是非连续的,非顺序存储的
链表的顺序是通过指针依次链接链表中的每个节点
链表中有一种双向链表,可以从一个节点查看它的下一个节点,也可以查看它的上一个节点
链表的定义
- 链表是线性结构
- 链表的存储方式是不连续的,是将零散的内存块串联起来
- 每个内存块称为节点Node, 除了存储数据,还需要存储指向下一个内存块的指针
- 存储方式,以head为头节点,头节点可以不存放任何数据,它是链表的开始,指向链表中的第一个节点,而每个节点都有一个next的向下引用,指向下一个节点,直到最后一个节点,每个节点由两部分组成,包括data, next
链表中常见的名词
- 首元节点,指的是链表中存储第一个数据元素的节点
- 头节点,它是在首元节点之前设置的一个节点,其指针指向首元节点
- 头指针,指向链表中的第一个节点的指针
使用链表的必要性
- 使用链表不需要预先知道大小,去申请内存,它实现动态管理内存
- 它的增删O(1), 随机访问O(n)
- 链表的增删只需要改变指针指向
- 随机访问需要每次都从头开始遍历
- 数组的缺点
- 需要申请连续空间,连续空间不足会申请失败
- 大小固定,空间不足需扩容的情况下需要进行数据复制
- 链表的缺点
- 占用内存过大,需要保存额外的指针
- 增删频繁的情况下,需要频繁的申请和释放内存,容器产生内存碎片
- 选择
- 访问操作 > 增删操作 - 数组
- 访问操作 < 增删操作 - 链表
常见的链表
- 单向链表
- 每个节点只包含一个后继指针
- 单链表有两个特殊的节点,即首节点和尾结点
- 性能,插入和删除节点 O(1), 查找复杂度O(n)
- 单向循环链表
- 尾结点的后继指针指向首节点的单向链表
- 双向链表
- 每个节点有两个节点,指向前一个节点地址prev和下一个节点 的指针next
- 首节点的前驱指针和尾结点的后继指针均指向空地址
- 性能特点
- 和单链表相比,存储相同的数据,需要消耗更多的存储空间,每个节点需要存储两个指针
- 插入、删除操作比单链表效率更高,O(1)级别
- 给定数据值删除对应的节点,单链表和双向链表,都需要从头到尾进行遍历,从而找到对应的节点进行删除,时间复杂度为O(n)
- 给定节点地址删除节点,要进行操作必须找到前驱节点,单链表需要从头到尾,进行遍历直到p.next = q, 时间复杂度为 O(n), 而双向链表可以直接找到前驱节点,时间复杂度为O(1)
- 对于一个有序链表,双向链表的按值查询效率要比单向链表高一些,可逆向查找,节省查找时间
- 双向循环链表
- 首节点的前驱指针指向尾结点,尾结点的后继指针指向首节点
单链表的类实现
class Node {constructor(el) {this.el = elthis.next = null}}class LinkedList {constructor() {this.head = nullthis.length = 0}append(el) {let node = new Node(el)if (this.length == 0) {this.head = node;} else {let current = this.head;while(current.next) {current = current.next;}current.next = node;}++this.length}insert(el, pos) {let current;let previous;let node = new Node(el)let index = 0;if (pos >= 0 && pos <= this.length) {if (pos == 0) {node.next = this.head;this.head = node;} else {let current = this.head;while(index < pos) {previous = current;current = current.next;index++}node.next = current;previous.next = node;}this.length++}}remove(el) {let pos = this.indexOf(el)return removeAt(pos)}removeAt(pos) {let previous;let current = this.head;let index = 0;if (pos > - 1 && pos < this.length) {if (pos == 0) {this.head = current.next;} else {while(index < pos) {previous = currentcurrent = current.nextindex++}previous.next = current.next;--this.length;return true;}}return false;}indexOf(el) {let index = 0;let current = this.head;while(current) {if (current.el == el) return index;current = current.next;index++}return index;}size() {return this.length;}isEmpty() {return this.length == 0;}}
- 判断链表中是否有环
const hasCycle = (head) => {if (head == null || head.next == null) {return false;}let slow = fast = head;while(fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next;if (fast == slow) {return true}}return false;}
小结
- 什么是链表
- 链表的优缺点, 增删快,查找慢
- 单向和双向链表
- 如何查找链表中的环
练习题
查找未知长度链表中倒数第K个元素
方法1
- 先遍历链表,获取链表长度
- 将链表长度 - k 获取要查找的索引值
- 再次遍历数组,获取到该索引位置的值;
- 若查找不到,返回-1
- 缺点,要遍历两次
/*输入: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11;*/function getLengthOfLinkedList(linkedList) {let length = 0;let current = linkedList.head;if (current == null) return length;while(current) {length++;current = current.next;}return length;}function searchNode(linkedList, k) {let index = 0;let resultIndex = getLengthOfLinkedList(linkedList) - k;let current = linkedList.headif (current == null) return -1;while(current) {if (index === resultIndex) return current.element;current = current.next;index++}return -1;}
方法2
- 定义两个指针
- 先让指针p1 向前移动 k - 1 步
- 遍历链表,如果p1 指向了尾结点,此时p2就是我们要找的节点
- 只需要遍历一次就可以查找到元素
/*输入: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11;*/function searchNode(linkedList, k) {let p1 = linkedList.headlet p2 = linkedList.headfor(let i = 0; i < k - 1; i++) {p1 = p1.next;}while(p1.next) {p1 = p1.next;p2 = p2.next;}return p2.elment;}console.log(searchNode(linkedList, 3)) // 9
