链表是一种线性表,但是不同于顺序表,它存储在不连续的内存空间上,可以更灵活的进行插入、删除的操作。
1 分类
1.1 单向链表 vs 双向链表
1.2 带傀儡节点的 vs 不带傀儡节点的
傀儡节点(dummy node):不存储实际使用的数据。
1.3 带环的 vs 不带环的
所谓带环的就是最后一个节点的 next 不指向 null,而是指向链表中的某个元素,一旦链表是带环的,就没有任何一个节点是指向 null 的。例如这里的第五个节点,它的后继不是空而是第三个节点,next 引用存储的是第三个节点的地址。
2 创建单链表
2.1 节点定义
public class Node {
public int val;
public Node next = null;
public Node(int val) {
this.val = val;
}
@Override
public String toString() {
return "[" + val +"]";
}
}
2.2 创建链表
public class Main {
public static Node createList() {
Node a= new Node(1);
Node b= new Node(2);
Node c= new Node(3);
Node d= new Node(4);
a.next = b;
b.next = c;
c.next = d;
// 可以不写,因为Node方法将next默认设为null
d.next = null;
return a;
}
public static void main(String[] args) {
Node head = createList();
}
}
方法结束后,a、b、c、d这四个局部变量就没了,但是因为方法返回第一个节点的地址给 head 了,然后后面的每一个节点都有前一个节点的 next 引用指向着,链表就还在,通过 head 就可以找到链表( 一个对象没有任何一个强引用指向,或者它不可达,就会被垃圾回收器回收 )。引用概念的理解对链表的理解很重要《Java笔记 (五)—— 引用类型》
3 单链表的遍历
public static void main(String[] args) {
Node head = createList();
for (Node cur = head; cur != null ; cur=cur.next) {
System.out.println(cur.val);
}
}
3.1 链表的最后一个节点
Node head = createList();
Node cur = head;
// 注意空链表的情况,解引用前要保证它非空
// 如果是没有傀儡节点的链表,空链表就是 head = null
// 如果是有傀儡节点的链表,空链表就是 head.next = null
while(cur != null && cur.next != null) {
cur = cur.next;
}
System.out.println(cur.val);
3.2 倒数第二个节点
Node cur = head;
while(cur != null
&& cur.next != null
&& cur.next.next != null) {
cur = cur.next;
}
System.out.println(cur.val);
3.3 第 N 个节点
int N = 3;
Node cur = head;
for (int i = 1; i < N; i ++) {
cur = cur.next;
}
System.out.println(cur.val);
3.4 倒数第 N 个节点
int count = 0;
int minusN = 1;
Node cur = head;
for (;cur != null;cur = cur.next) {
count ++;
}
int plusN = count + 1 - minusN;
cur = head;
for (int i = 0; i < plusN; i++) {
cur = cur.next;
}
System.out.println(plusN);
3.5 获取链表长度
int count = 0;
for (Node cur = head;cur!= null ; cur = cur.next) {
count ++;
}
System.out.println(count);
3.6 查找链表上是否有某个元素
int toFind = 5;
Node cur = head;
for (;cur != null;cur = cur.next) {
if(cur.val == toFind) {
break;
}
}
if (cur != null) {
System.out.println("true");
} else {
System.out.println("false");
}
4 插入操作
4.1 不带傀儡节点
public static void traversal(Node head) {
for(Node cur = head;cur != null;cur = cur.next) {
System.out.print(cur.val + " ");
}
}
// 1 插入到中间位置
Node head = createLinkedList();
Node one = head;
//Node newNode = new Node(50);
//newNode.next = one.next;
//one.next = newNode;
// 2 插入到最头部
//Node newNode = new Node(70);
//newNode.next = head;
//head = newNode;
//traversal(head);
// 3 插入到尾部
Node newNode = new Node(90);
Node prev = head;
while (prev != null && prev.next != null) {
prev = prev.next;
}
prev.next = newNode;
traversal(head);
4.2 带傀儡节点
// 插入数据,插到头部、中间都一样
Node head = createLinkedListWithDummy();
Node newNode = new Node(110);
// 1 和 2 之间
//Node prev = head.next;
//newNode.next = prev.next;
//prev.next = newNode;
//traversalWithDummy(head);
// 插入到头部
Node prev = head;
newNode.next = prev.next;
prev.next = newNode;
traversalWithDummy(head);
5 删除操作
5.1 无傀儡节点
// 1 按照值删除
// 找到该值对应的位置
// 同时找到前一个位置
public static void remove1(Node head,int val) {
if (head == null) {
return head;
}
if (head.val == val) {
head = head.next;
return head;
}
Node prev = head;
while (prev.next != null && prev.next.val == val) {
prev = prev.next;
}
if(prev.next == null) {
System.out.println("未找到");
return null;
}
Node toDelete = prev.next;
// 真正进行删除,toDelete 指向要被删除的节点
// prev.next 指的是要删除的节点
// prev 指的是要删除的前一个节点
prev.next = toDelete.next;
return head;
}
// 2 按照位置删除
//(基础做法,时间复杂度 O(N) )
public static Node remove2(Node head,Node toDelete) {
// 时间复杂度 O(N)
if (head == null) {
return head;
}
if (head == toDelete) {
head = head.next;
return head;
}
Node prev = head;
while ( prev.next != null && prev.next != toDelete) {
prev = prev.next;
}
if (prev.next == null) {
System.out.println("未找到");
return null;
}
prev.next = toDelete.next;
return head;
// 按照位置删除 优化 (时间复杂度 O(1))
// toDelete.val = toDelete.next.val;
// toDelete.next = toDelete.next.next;
// return head;
// 但如果是删除最后一个位置的节点,不能用这个方法
// 要用上一个方法
}
// 3 按照下标删除
// 其实就是给定第几个(从第 0 个开始)
public static Node remove3(Node head,int index) {
// 按照下标删除,其实就是按照给定第几个
if(index < 0 || index >= size(head)) {
return null;
}
if(index == 0) {
head = head.next;
return head;
}
Node prev = head;
for (int i = 1; i < index; i++) {
prev = prev.next;
}
Node toDelete = prev.next;
prev.next = toDelete.next;
return head;
}
private static int size(Node head) {
int size = 0;
for(Node cur = head;cur != null;cur = cur.next) {
size++;
}
System.out.println(size);
return size;
}
// 4 删除尾部节点
public static Node removeTail(Node head) {
if(head == null ){
return null;
}
Node prev = head;
while (prev.next != null && prev.next.next != null) {
prev = prev.next;
}
prev.next = null;
return head;
}
// 5 测试无傀儡节点的删除
// head = remove1(head,1);
// traversal(head);
// int num = 1;
// Node cur = head;
// while (cur != null && cur.val != num) {
// cur = cur.next;
// }
// head = remove2(head,cur);
// traversal(head);
// head = remove3(head,0);
// traversal(head);
// head = removeTail(head);
// traversal(head);
注意:
涉及到头节点的操作时,如果仅仅在方法中对形参 " head " 进行修改,在方法结束后,形参就会被销毁,实际的 " head " 并没有被改变。这时就要通过返回值把 " head " 的值返回回去。
5.2 有傀儡节点
// 1 按值删除
public static void removeWithDummy1(Node head,int val) {
Node prev = head;
while (prev.next != null && prev.next.val != val) {
prev = prev.next;
}
Node toDelete = prev.next;
prev.next = toDelete.next;
}
// 2 按位置删除
public static void removeWithDummy2(Node head,Node toDelete) {
Node prev = head;
while (prev.next != null && prev.next != toDelete) {
prev = prev.next;
}
if (prev.next == null) {
System.out.println("未找到,链表内容为:");
return;
}
toDelete = prev.next;
prev.next = toDelete.next;
}
// 3 按下标删除(从 0 开始)
public static void removeWithDummy3(Node head,int index) {
Node prev = head;
if(index < 0 || index > sizeWithDummy(head)) {
System.out.println("未找到,链表内容为:");
return;
}
for (int i = 0; i < index ; i++) {
prev = prev.next;
}
Node toDelete = prev.next;
prev.next = toDelete.next;
}
public static int sizeWithDummy(Node head) {
int size = 0;
for(Node cur = head.next;cur != null;cur = cur.next) {
size ++;
}
System.out.println(size);
return size;
}
// 4 测试有傀儡节点的删除
// Node head = createLinkedListWithDummy();
// removeWithDummy1(head,5);
// traversalWithDummy(head);
// int num = 6;
// Node cur = head.next;
// while (cur != null && cur.val != num) {
// cur = cur.next;
// }
// removeWithDummy2(head,cur);
// traversalWithDummy(head);
// removeWithDummy3(head,0);
// traversalWithDummy(head);
相比之下有傀儡节点的比没有傀儡节点的代码较简单,因为有傀儡节点就不用改变" head ",避免一些容易出错的地方。
6 实现一个自己的双向链表
class ListNode {
int val;
ListNode prev = null;
ListNode next = null;
public ListNode(int val) {
this.val = val;
}
}
// 实现一个双向链表
public class MyLinkedList {
// 记录头、尾节点的位置
private ListNode head;
private ListNode tail;
// 记录长度,空间换时间
private int length = 0;
public MyLinkedList() {
this.head = null;
this.tail = null;
}
public int length(){
return this.length;
}
// 1. 插入操作
// 1.1 头插
public void addFirst(int val){
ListNode newNode = new ListNode(val);
if (head == null) {
head = newNode;
tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
length ++;
}
// 1.2 尾插
public void addLast(int val) {
ListNode newNode = new ListNode(val);
if (head == null) {
head = newNode;
tail = newNode;
} else {
tail.next = newNode;
newNode.prev = tail;
tail = tail.next;
}
length ++;
}
// 1.3 指定位置插入
public void add(int index,int val) {
if (index < 0 || index >= length) {
return;
}
if (index == 0) {
addFirst(val);
}
if (index == length) {
addLast(val);
}
ListNode newNode = new ListNode(val);
ListNode nextNode = getNode(index);
ListNode prevNode = getNode(index - 1);
newNode.next = nextNode;
nextNode.prev = newNode;
newNode.prev = prevNode;
prevNode.next = newNode;
length ++;
}
// 2. 删除操作
// 2.1 按下标删除
public void removeByIndex(int index) {
if (index < 0 || index > length) {
return ;
}
if (index ==0) {
removeFirst();
return;
}
if (index == length - 1) {
removeLast();
return;
}
ListNode toDelete = getNode(index);
ListNode prevNode = toDelete.prev;
ListNode nextNode = toDelete.next;
prevNode.next = nextNode;
nextNode.prev = prevNode;
length --;
}
// 2.2 按值删除
public void removeByValue(int val) {
int index = indexOf(val);
if (index == -1){
return ;
}
removeByIndex(index);
}
// 2.3 头删
public void removeFirst() {
if (head == null){
return ;
}
if (head.next == null) {
head = null;
tail = null;
length = 0;
return ;
}
ListNode nextNode = head.next;
nextNode.prev = null;
head = nextNode;
length --;
}
// 2.4 尾删
public void removeLast() {
if (head == null){
return ;
}
if (head.next == null) {
head = null;
tail = null;
length = 0;
return ;
}
ListNode prevNode = tail.prev;
prevNode.next = null;
tail = prevNode;
length --;
}
// 3. 查找
// 3.1 给定下标去找节点
public ListNode getNode(int index) {
if (index < 0 || index >= length) {
return null;
}
ListNode cur = head;
for (int i = 0; i <= index; i++) {
cur = cur.next;
}
return cur;
}
// 3.2 给定下标去找值
public int indexOf(int index) {
return getNode(index).val;
}
//3.3 给定值去找下标
public int getVal(int val) {
ListNode cur = head;
for (int i = 0; i < length; i++) {
if (cur.val == val) {
return i;
}
cur = cur.next;
}
return -1;
}
// 4. 修改
public void set(int index,int val) {
ListNode listNode = getNode(index);
listNode.val = val;
}
}