目录
-
自身的特点
- 不需要连续的内存空间,它可以通过指针将零散的内存碎片串联起来使用
- 每个零散的内存块相当于一个节点,为了将每个节点都串联起来,每个节点除了需要存储数据外,还需要记录链表上的下个节点的地址,记录下个节点地址的指针叫做后继指针
-
适合解决的问题
- 数据插入
- 并不需要为了保持内存的连续性而搬移节点
- 时间复杂度 O(1)
- 注意如果从中间指定位置插入,虽然插入时间复杂度是 O(1),但插入前需要查找这个位置,这个过程是O(n)的,根据复杂度中的加法法则,插入的时间复杂度是O(n)
- 数据删除
- 并不需要为了保持内存的连续性而搬移节点
- 时间复杂度 O(n)
- 注意如果从中间指定位置删除,虽然删除时间复杂度是 O(1),但删除前需要查找这个位置,这个过程是O(n)的,根据复杂度中的加法法则,删除的时间复杂度是O(n)
- 删除时,必须知道删除节点的前驱节点,因为要改变前驱节点的下一个节点,这个过程也是O(n),因为要遍历整个链表
- 数据插入
-
时间复杂度
- 插入与删除 O(n)
- 随机访问 O(n)
-
不适合的应用场景
- 随机访问
- 有序内存不是连续性的,所以就不能通过下标随机访问,随机访问只能遍历各个节点,在找到对应位置的节点
- 时间复杂度 O(n)
- 随机访问
-
链表常用类型
-
单链表
- 头结点:记录链表的基地址,有了它,可以遍历整个链表
- 尾结点:最后一个节点,如果指针的下一节点地址是空地址NULL,说明这个节点是最后一个节点
- 前驱节点:无,找到前驱节点必须在遍历一边链表,时间复杂度 O(n)
-
循环链表
- 尾结点:最后一个节点的下一节点地址是头结点
-
双向链表
- 常用
- 单链表只有一个方向,每个节点只有下一节点指针,无法定位上一节点,双向链表顾名思义,不但有下一节点指针,还有上一节点指针(prev)
- 优点:找到前驱节点,时间复杂度O(1),这是个优势,单向链表在插入删除时,找打前驱节点的时间复杂度是 O(n) ,双向链表就优化了这点
- 缺点:占用空间比单向链表大,因为会记录个前驱节点。
- java 的LinkedHashMap 用的就是双向链表
-
单向链表和双向链表如何选择
- 内存空间小,采用单向链表,以时间换空间
- 内存空间足够,在意时间,用双向链表,以空间换时间
-
双向循环链表
- 双向链表的尾结点指向头结点
-
链表 VS 数组
- 内存使用苛刻,选数组,链表会有额外的空间存储指针,并且对链表频繁的删除、插入,会导致内存频繁的申请与释放,容易造成内存碎片,java语言就会频繁的GC
- 数组大小固定,容易越界
-
检查链表的常用边界
- 如果链表为空时,链表是否能正常工作
- 如果链表只包含一个节点时,链表是否能正常工作
- 如果链表只包含两个节点时,链表是否能正常工作
- 代码逻辑在处理头结点和尾节点时,代码是否能正常工作
-
代码
-
代码练习1 (反转单链表)
public class Node {
private int data;
private Node next;
public Node(int data) {
this.data = data;
}
protected Node() {
}
protected int getData() {
return data;
}
protected void setData(int data) {
this.data = data;
}
protected Node getNext() {
return next;
}
protected void setNext(Node next) {
this.next = next;
}
}
/**
* 反转单链表
*/
public class Reverse {
static Node head = new Node();
private void insert(int value) {
Node node = new Node(value);
if (head == null) {
head.setNext(node);
}
node.setNext(head.getNext());
head.setNext(node);
}
private Node reverse() {
Node pre = head;
Node curr = new Node();
Node next;
while (pre.getNext() != null) {
// 将原链表的首节点拿出来
next = pre.getNext();
// 将首节点的下一节点定义为首节点
pre.setNext(next.getNext());
next.setNext(curr.getNext());
curr.setNext(next);
}
return curr;
}
private void print(Node node) {
Node tmp = node.getNext();
while (tmp != null) {
System.out.println(tmp.getData());
tmp = tmp.getNext();
}
}
public static void main(String[] args) {
Reverse reverse = new Reverse();
for (int i = 0; i < 11; i++) {
reverse.insert(i);
}
reverse.print(reverse.reverse());
}
}
-
代码练习2(检测是否是循环链表)
/**
* 检测是否是循环链表
*/
public class CheckRing {
static Node head = new Node();
// 循环链
private void insert(int value) {
Node node = new Node(value);
if (head == null) {
head.setNext(node);
node.setNext(head);
}
node.setNext(head.getNext());
head.setNext(node);
}
private Boolean checkRing() {
// 慢, 这个节点会慢慢的前进
Node slow = head;
// 这个节点会快点前进
Node fast = head.getNext();
// 如果存在环, 快节点是不会存在null的
while (fast.getNext() != null && fast != null) {
slow = slow.getNext();
fast = fast.getNext().getNext();
// 当快节点和慢节点相遇, 说明有环
if (slow == fast) {
return true;
}
}
return false;
}
}
-
代码练习3(合并两个有序链表)
/**
* 合并链表
*/
public class MergeLink {
/**
* 合并两个有序了列表
*
* @param link1 第一个有序列表, 没有头结点
* @param link2 第二个有序列表, 没有头结点
* @return
*/
private Node merge(Node link1, Node link2) {
// 申请一个头节点
Node newLink = new Node(0);
// 让这个节点不断的去变化
Node tmp = newLink;
while (link1 != null && link2 != null) {
if (link1.getData() < link2.getData()){
tmp.setNext(link1);
link1 = link1.getNext();
}else {
tmp.setNext(link2);
link2 = link2.getNext();
}
tmp = tmp.getNext();
}
return newLink.getNext();
}
}
-
代码练习4(删除链表倒数第n个节点)
/**
* 删除链表倒数第 n 个节点
*/
public class DeleteNode {
Node head = new Node();
// 删除倒数第n个节点
private Node deleteNodeOfReciprocal(Node link, int n) {
// 一次遍历法
// 两个节点一直保持这1的差距,当快节点的下一节点是空时,返回慢节点
Node slow = link;
Node fast = link;
int i = 1;
while (i < n) {
fast = fast.getNext();
i++;
}
while (fast.getNext() != null) {
fast = fast.getNext();
slow = slow.getNext();
}
return slow;
}
}
-
代码练习5(查找链表中间节点)
/**
* 查找中间节点
*/
public class MiddleNode {
private Node findMiddleNode(Node list) {
// 两个节点,一个快一个慢,快的一次走两步,慢的一次走一步,快的走完后,去慢节点
Node fast = list;
Node slow = list;
while (fast != null && fast.getNext() != null) {
fast = fast.getNext().getNext();
slow = slow.getNext();
}
return slow;
}
}