数据结构--链表
1. 链表原理
顺序表是元素在连续的内存空间上, 只要顺着内存继续往下走, 就能找到下一个节点
链表是在不连续的内存上
元素(element): 真实存于线性表中的内容,是我们关心的核心内容。
结点(node): 为了组织链表而引入的一个结构,除了保存我们的元素之外,还会保存指向下一个结点的引用
class Node {
int val; // 保存我们的元素
Node next; // 保存指向下一个结点的引用;其中尾节点的 next == null
}
当前结点(current / cur): 表示链表中某个结点。
前驱结点(previous / prev): 表示链表中某个结点的前一个结点;头结点没有前驱结点。
后继结点(next): 表示链表中某个结点的后一个结点;尾结点没有后继结点。
链表的头结点, 链表最开始的节点~
尤其是对单链表来说, 只要知道了链表的头结点就可以获取到链表的所有的元素!
通常情况下,特别喜欢用头结点来代指整个链表~
2. 链表种类
2.1 单向与双向链表
单向链表:只能够通过当前节点,找到下一个节点~,无法找到上一个节点
双向链表:通过当前节点,能找到下一个节点,也能找到上一个节点
2.2 带环与不带环链表
带环的链表: 最后一个元素不指向 null ,而是指向链表上的某个节点
不带环的链表: 最后一个元素指向 null
2.3 带傀儡节点与不带傀儡节点链表
dummy node 傀儡节点: 没啥卵用,不起实际作用 不实际存储数据,只是用来占个位置引用傀儡节点,目的是让代码写起来更简单~~
head node 头结点: 链表的第一个节点~
如果是不带傀儡节点的链表,表示空链表,就直接用 head =null 来表示
如果是带傀儡节点的链表,表示空链表(带傀儡节点,意味着无论如何)
衍生出8种链表
- 单向,带傀儡节点,带环的链表
- 单向,带傀儡节点,不带环的链表
- 单向,不带傀儡节点,带环的链表
- 单向,不带傀儡节点,不带环的链表
- 双向,带傀儡节点,带环的链表
- 双向,带傀儡节点,不带环的链表
- 双向,不带傀儡节点,带环的链表
- 双向,不带傀儡节点,不带环的链表
链表存在的最大意义:
相比于顺序表来说,链表往中间位置插入或者删除元素,都可以避免搬运,效率是比较高~
3. 链表的实现
3.1 定义节点
public class Node {
int val;
Node next;
public Node(int val) {
this.val = val;
}
@Override
public String toString() {
return "[" +
val +
']';
}
}
3.2 链表的创建
Node n1 = new Node(1);
Node n3 = new Node(3);
Node n2 = new Node(2);
Node n6 = new Node(6);
n1.next = n3;
n3.next = n2;
n2.next = n6;
n6.next = null;
Node head = n1;
4. 实现双向链表
class Node {
int val;
Node prev = null;
Node next = null;
@Override
public String toString() {
return "{" + val +
'}';
}
public Node(int val) {
this.val = val;
}
}
// 实现一个双向链表
public class MyLinkedList {
// 记录头结点位置
private Node head;
// 记录尾节点位置
private Node tail;
// 链表元素个数
private int length;
public MyLinkedList() {
head = null;
tail = null;
}
public int length() {
return this.length;
}
// 插入节点
// 头插
public void addFirst(int val) {
Node newNode = new Node(val);
// 空链表
if (head == null) {
head = newNode;
tail = newNode;
length++;
return;
}
// 非空的情况
newNode.next = head;
head.prev = newNode;
head = newNode;
length++;
return;
}
// 尾插
public void addLast(int val) {
Node newNode = new Node(val);
// 空链表
if (head == null) {
head = newNode;
tail = newNode;
length++;
return;
}
// 非空链表
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
length++;
}
// 指定位置插入
public void add(int index, int val) {
// 先处理特殊情况
if (index < 0 || index > length) {
return;
}
// 处理头插
if (index == 0) {
addFirst(val);
return;
}
// 处理尾插
if (index == length) {
addLast(val);
return;
}
// 考虑一般情况
// 此时需先找到下标
Node nextNode = getNode(index);
// 需要在 nextNode 之前插入
Node newNode = new Node(val);
Node prevNode = nextNode.prev;
prevNode.next = newNode;
newNode.prev = prevNode;
newNode.next = nextNode;
nextNode.prev = newNode;
length++;
return;
}
public void removeFirst() {
// 考虑特殊情况
if (head == null) {
return;
}
if (head.next == null) {
head = null;
tail = null;
length = 0;
return;
}
// 删除头结点
Node nextNode = head.next;
nextNode.prev = null;
head = nextNode;
length--;
}
public void removeLast() {
if (head == null) {
return;
}
if (head.next == null) {
head = null;
tail = null;
length = 0;
return;
}
Node prevNode = tail.prev;
prevNode.next = null;
tail = prevNode;
length--;
}
// 删除
public void removeByIndex(int index) {
if (index < 0 || index >= length) {
return;
}
// 头删
if (index == 0) {
removeFirst();
return;
}
// 尾删
if (index == length - 1) {
removeLast();
return;
}
// 根据下标,找位置
Node toRemove = getNode(index);
// 记录前后位置
Node prevNode = toRemove.prev;
Node nextNode = toRemove.next;
// 删除节点
prevNode.next = nextNode;
nextNode.prev = prevNode;
length--;
}
// 按照值删除
public void removeByValue(int val) {
int index = indexOf(val);
if (index == -1) {
// 未找到 val
return;
}
removeByIndex(index);
}
// 根据下标找节点
public Node getNode(int index) {
if (index < 0 || index >= length) {
return null;
}
Node cur = head;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
return cur;
}
// 查找
public int get(int index) {
if (index < 0 || index >=length) {
throw new ArrayIndexOutOfBoundsException();
}
return getNode(index).val;
}
public int indexOf(int value) {
Node cur = head;
for (int i = 0; i < length; i++) {
if (cur.val == value) {
return i;
}
cur = cur.next;
}
// 未找到
return -1;
}
// 修改
public void set(int index, int value) {
if (index < 0 || index >= length) {
throw new ArrayIndexOutOfBoundsException();
}
Node node = getNode(index);
node.val = value;
}
public static void main(String[] args) {
Node head = creatLinkedLisr();
printNode(head);
// 头插
}
private static void printNode(Node head) {
// Node cur = head;
for (; head != null; head =head.next ) {
System.out.print(head.toString());
}
}
private static Node creatLinkedLisr() {
Node a = new Node(2);
Node b = new Node(3);
Node c = new Node(4);
Node d = new Node(5);
Node e = new Node(6);
Node f = new Node(7);
a.next = b;
b.next = c;
c.next = d;
d.next = e;
e.next = f;
f.next =null;
f.prev = e;
e.prev = d;
d.prev = c;
c.prev = b;
b.prev = a;
a.prev = null;
return a;
}
}
5. 注意
- 链表的话由于不是数组,无法用 index 来直接取下标,实际上实现的时候,也可以实现 get / set
方法来取下标,只不过比顺序表取下标操作要低效一些(Java标准库中的LinkedList 就是这么做的) - 链表操作中,很多操作都是基于引用来进行的
顺序表:
- 空间连续、支持随机访问
- 中间或前面部分的插入删除时间复杂度O(N)
- 增容的代价比较大。
链表:
- 以节点为单位存储,不支持随机访问
- Java 中 LinkedList 的 add , remove 都需要先定位置, 遍历链表,才可以插入, 单从插入, 删除本身时间复杂度:O(1), 但加上遍历就是 O(N)
- 没有增容问题,插入一个开辟一个空间。