目录
1. 链表
1.1 单向链表
1.2 单链表增删查改
增
查
改
删
2.1 带头单链表
2.2 带头单链表增删查改
增
查
改
删
3.1 双向链表
3.2 双向链表的增删查改
增
查
改
删
1. 链表
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。
1.1 单向链表
单向链表:每个节点只保存了下一个节点的地址,只能从头节点开始向后遍历,这种链表结构称为单向链表,简称单链表。
我们将单链表类比为火车:火车的不同车厢之间,都是通过一个挂钩连接的,当这两个这厢脱钩后,这两个车厢就没有任何关系了。
定义链表类 ——> 类比为火车
public class SingleLinedList {
Node head; //头节点(第一个车厢)
int size; //当前链表中的节点个数,保存的元素个数(一个Node节点只保存了一个值)
}
给定火车的火车头 Node head ,就能从head开始向后遍历,遍历当前这个链表中所有的节点。
定义节点类 ——> 火车的每节车厢
class Node {
int val; //当前节点保存的值
Node next; //下一个节点的地址
}
每—列火车由若干个车厢组成,每个车厢就是一个 Node 对象,由多个 Node 对象组成的大实体就是链表对象。
toString方法 —— 用字符串格式化输出链表
public String toString() {
String ret = "";
for (Node x = head; x != null; x = x.next) {
ret += x.val;
ret += "——>";
x = x.next;
}
ret += "Null";
return ret;
}
1.2 单链表增删查改
增
public void addFirst(int val):向当前的链表中添加一个新的节点,默认在链表的头部添加
/**
* 向当前的链表中添加一个新的节点,默认在链表的头部添加
*/
public void addFirst(int val) {
Node newNode = new Node();
newNode.val = val;
if (head != null) {
newNode.next = head;
}
//无论是否为空,插入后都是链表的新节点
head = newNode;
size++;
}
public void add(int index,int val):在链表index处插入一个新节点
/**
* 在链表index处插入一个新节点
*/
public void add(int index, int val) {
//判断index插入位置是否非法
if (index < 0 || index > size) {
System.err.println("add index illegal!");
}
//头节点没有前驱
if (index == 0) {
//头插
addFirst(val);
} else {
//当前位置合法且不是在链表头部插入
Node newNode = new Node();
newNode.val = val;
Node prev = head;
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
//此时prev一定指向了待插入的前驱
newNode.next = prev.next;
prev.next = newNode;
size++;
}
}
注意:在添加节点的插入index位置判断时,可以使index=size,相当于在最后一个有效元素的后一个位置插入。而在查找、修改、删除时,index不能取到size,因为size索引上没有有效元素。
public void addLast(int val):在链表的尾部插入新元素
/**
* 尾插
*/
public void addLast(int val) {
add(size, val);
}
相当于在最后一个有效元素的后一个位置插入。
查
public int getByValue(int val):查找第一个值为val的结点索引是多少
public int getByValue(int val) {
int index = 0;
for (Node x = head; x != null; x = x.next) {
if (x.val == val) {
return index;
}
index++;
}
//链表中没有值为val的节点,返回-1
return -1;
}
private boolean rangeCheck(int index):判断index位置的合法性
/**
* 判断index位置的合法性 rangeCheck
*/
private boolean rangeCheck(int index) {
//size是当前有效元素的下一个索引
if (index < 0 || index >= size) {
return false;
}
return true;
}
public boolean contains(int val):查询当前链表中是否包含值为 val 的元素,若存在返回true,否则返回false
/**
* 查询当前链表是否含有值为val的元素
*/
public boolean contains(int val) {
//第一种写法
// Node prev = head;
// while (prev != null) {
// if (prev.val == val) {
// return true;
// }
// prev = prev.next;
// }
// return false;
//第二种写法
// int index = getByValue(val);
// if (index == -1) {
// return false;
// }
// return true;
//第三种写法
return getByValue(val) != -1;
}
public int get(int index):查询索引为index位置的结点值
/**
* 查询索引为index的元素值为多少
*/
public int get(int index) {
//index 合法性判断 另写一个私有方法 rangeCheck(index) 专门判断合法性
// 1.判断index的合法性
if (rangeCheck(index)) {
Node x = head;
for (int i = 0; i < index; i++) {
x = x.next;
}
return x.val;
}
System.err.println("index illegal!set error!");
return -1;
}
改
public int set(int index,int newVal):修改索引为index位置的结点值为newVal,返回修改前的节点值
/**
* 修改索引为index位置的值为newVal,返回修改前的值
*/
public int set(int index, int newVal) {
if (rangeCheck(index)) {
Node x = head;
for (int i = 0; i < index; i++) {
x = x.next;
}
int oldVal = x.val;
x.val = newVal;
return oldVal;
}
System.err.println("index illegal!set error");
return -1;
}
删
public void removeValueOnce(int val):删除链表中第一个值为val的元素
/**
* 删除第一次出现的值为val的节点
*/
public void removeValueOnce(int val) {
if (head == null) {
System.err.println("链表为空,无法删除");
return;
}
if (head.val == val) {
// 此时头结点就是第一个值为val的结点
Node x = head;
head = head.next;
x.next = null;
size--;
} else {
// 当前头结点不是待删除的结点,需要遍历
// 这里同样找到待删除的前驱结点
// 能否知道前驱结点移动步数?
Node prev = head;
while (prev.next != null) {
// 至少还有后继结点
if (prev.next.val == val) {
// 此时prev.next就是待删除的结点
Node node = prev.next;
prev.next = node.next;
node.next = null;
size--;
return;
}
prev = prev.next;
}
}
}
public int remove(int index):删除单链表中索引为index位置的节点,返回删除前的节点值
/**
* 删除索引为index位置的操作,返回删除前的节点值
*/
public int remove(int index) {
if (rangeCheck(index)) {
if (index == 0) {
//删除头节点
Node x = head;
head = head.next;
size--;
x.next = null;
return x.val;
} else {
//此时删除的是链表的中间结点
//找前驱
Node prev = head;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
// prev就是待删除节点的前驱
// node节点就是待删除的结点
Node node = prev.next;
prev.next = node.next;
node.next = null;
size--;
return node.val;
}
}
System.err.println("remove index illegal!");
return -1;
}
public void removeAllValue(int val):删除链表中所有值为val的结点
/**
* 删除链表中 所有值为val 的结点
*/
public void removeAllValue(int val) {
while (head != null && head.val == val) {//2,2,2,2极端情况
// 头节点就是待删除节点
Node x = head;
head = head.next;
x.next = null;
size --;
}
// 头节点一定不是待删除的节点
// 判空
if (head == null) {
// 链表删完了
return;
} else {
Node prev = head;
while (prev.next != null) {
// 至少还有后继结点
if (prev.next.val == val) {
// 此时prev.next就是待删除的结点
Node node = prev.next;
prev.next = prev.next.next;
node.next = null;
size--;
} else {
// 只有当prev.next.val != val才能移动prev指针!
prev = prev.next;
}
}
}
}
递归实现
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足Node.val == val 的节点,并返回新的头节点。
//不带头节点的删除(递归)
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return null;
}
head.next = removeElements(head.next,val);
if (head.val == val) {
return head.next;
}
return head;
}
2.1 带头单链表
虚拟头节点dummyHead,类似于现实中的火车头,只作为链表的头节点使用,不存储有效数据。链表中的其他有效节点我都可以一视同仁,全都有前驱节点。
定义带头链表类 ——> 类比为火车
public class LinkedListWithHead {
private int size; //当前链表有效结点的个数,不包括头结点
private Node dummyHead = new Node(); //虚拟头结点,实实在在存在,不存储数据,就作为火车头
}
给定虚拟头节点dummyHead(火车头),就能从dummyHead开始向后遍历,遍历当前这个链表中所有的节点。
2.2 带头单链表增删查改
增
public void addFirst(int val):向当前的链表中添加一个新的节点,默认在链表的头部添加
/**
* 头插
*/
public void addFirst(int val) {
//第一种写法
// Node node = new Node();
// node.val = val;
// node.next = dummyHead.next;
// dummyHead.next = node;
//第二种写法
// Node node = new Node(val, dummyHead.next);
// dummyHead.next = node;
//第三种写法
dummyHead.next = new Node(val,dummyHead.next);
size++;
}
有虚拟头节点之后,所有节点都是该节点的"后继"节点。无论当前是否存在有效元素,我们的插入步骤完全一致,省去了判断头的情况。
public void add(int index,int val):在链表index处插入一个新节点
/**
* 在索引为index的位置插入新元素val
*/
public void add(int index, int val) {
if (index < 0 || index > size) {
System.err.println("index illegal");
return;
}
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
//此时prev位置就是待插入位置
//1.普通插入
// Node node = new Node();
// node.val = val;
// node.next = prev.next;
// prev.next = node;
// 2.使用构造方法在产生新节点的同时给node赋值以及链接后继节点
// Node node = new Node(val,prev.next);
// prev.next = node;
// 3.使用匿名对象
prev.next = new Node(val, prev.next);
size++;
}
索引是从第一个有效元素开始计数的
dummyHead ——>不算索引。不存储有效元素
因为prev从dummyHead开始走,prev指针走index步恰好就是待插入位置的前驱。
查
public int get(int val):查找第一个值为val的结点索引是多少
/**
* 查找索引为index位置的值
*/
public int get(int index) {
if (index < 0 || index >= size) {
System.err.println("get index illegal!");
return -1;
}
Node prev = dummyHead.next;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
return prev.val;
}
改
public int set(int index,int newVal):修改索引为index位置的结点值为newVal,返回修改前的节点值
/**
* 修改索引为index位置值为newVal,返回修改之前的值
*/
public int set(int index, int newVal) {
if (index < 0 || index >= size) {
System.err.println("set index illegal!");
return -1;
}
Node prev = dummyHead.next;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
int oldVal = prev.val;
prev.val = newVal;
return oldVal;
}
删
public int remove(int index):删除单链表中索引为index位置的节点,返回删除前的节点值
/**
* 删除单链表中索引为index位置的节点值,返回删除前的值
*/
public int remove(int index) {
//index合法性
if (rangeCheck(index)) {
Node prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
Node node = prev.next;
int val = node.val;
prev.next = node.next;
node.next = node = null;
size--;
return val;
}
System.err.println("index illegal!");
return -1;
}
public void removeAllValue(int val):删除链表中所有值为val的结点
/**
* 删除所有值为val的元素
*/
public void removeAllValue(int val) {
Node prev = dummyHead;
while (prev.next != null) {
if (prev.next.val == val) {
// Node node = prev.next;
// prev.next = node.next;
// node.next = null;
prev.next = prev.next.next;
size--;
} else {
prev = prev.next;
}
}
}
此时不用再关心头节点的情况
prev都是从dummyHead开始向后判断,其他的逻辑和单链表的删除所有值完全一样,无需处理头节点的情况!
3.1 双向链表
双向链表:对于该链表中的任意节点,即可通过该节点向后走,也可以通过该节点向前走。
双向链表的节点:每个节点既保存了下一个节点的地址,也保存了上—个节点的地址。这样的话,就可以通过任意的节点从前向后或者从后向前。定义双向链表类
public class DoubleLinkedList {
//有效节点的个数
private int size;
//当前头结点
private DoubleNode head;
//当前尾节点
private DoubleNode tail;
}
定义双向链表的节点类
class DoubleNode {
//前驱节点
DoubleNode prev;
//当前节点值
int val;
//后继节点
DoubleNode next;
public DoubleNode() {
}
public DoubleNode(int val) {
this.val = val;
}
public DoubleNode(DoubleNode prev, int val, DoubleNode next) {
this.prev = prev;
this.val = val;
this.next = next;
}
}
3.2 双向链表的增删查改
增
public void addFirst(int val):向当前的链表中添加一个新的节点,默认在链表的头部添加
/**
* 头插
*/
public void addFirst(int val) {
//创建新节点
// 这个节点就是以后的头结点
// 构造方法为对象属性进行初始化
DoubleNode node = new DoubleNode(null, val, head);
if (head == null) {
tail = node;
} else {
head.prev = node;
}
// 对于头插来说,最终无论链表是否为空。head = node
head = node;
size++;
}
public void addLast(int val):向当前的链表中添加一个新的节点,默认在链表的尾部添加
/**
* 尾插法
*/
public void addLast(int val) {
//这个节点就是插入后的尾节点
DoubleNode node = new DoubleNode(tail, val, null);
if (tail == null) {
head = node;
} else {
tail.next = node;
}
tail = node;
size++;
}
public void add(int index,int val):在链表index处插入一个新节点
/**
* index 位置插入节点 (重点)
*/
public void add(int index, int val) {
if (index < 0 || index > size) {
System.err.println("add index illegal!");
return;
}
if (index == 0) {
addFirst(val);
} else if (index == size) {
addLast(val);
} else {
DoubleNode prev = node(index - 1);
DoubleNode next = prev.next;
DoubleNode node = new DoubleNode(prev, val, next);
prev.next = node;
next.prev = node;
size++;
}
}
此处的代码先后顺序无所谓。
查
private DoubleNode node(int index):根据索引值找节点
假设链表有100个节点,我想在97号索引位置插入新元素,从尾节点向前遍历就会快得多!
index < size / 2,从前向后找,插入位置在前半部分
index > size / 2,从后向前找,插入位置就在后半部分链表/**
* 根据索引值找节点
*/
private DoubleNode node(int index) {
DoubleNode x = null;
if (index < size / 2) {
x = head;
for (int i = 0; i < index; i++) {
x = x.next;
}
} else {
x = tail;
for (int i = size - 1; i > index; i--) {
x = x.prev;
}
}
return x;
}
public int get(int index):查询索引为index位置的结点值
/**
* 查找索引为index位置的值
*/
public int get(int index) {
if (index < 0 || index >= size) {
System.err.println("add index illegal!");
return -1;
}
DoubleNode cur = node(index); //见“查”部分的代码
return cur.val;
}
改
public int set(int index, int newVal):修改索引为index位置值为newVal,返回修改之前的值
/**
* 修改索引为index位置值为newVal,返回修改之前的值
*/
public int set(int index, int newVal) {
if (index < 0 || index >= size) {
System.err.println("add index illegal!");
return -1;
}
DoubleNode cur = node(index);
int oldVal = cur.val;
cur.val = newVal;
return oldVal;
}
删
private void unlink(DoubleNode node):删除当前双向链表中的node节点
分治思想
先处理前驱节点,完全不管后继,等前驱部分全部处理完毕,再单独处理后继情况。
/**
* 删除当前双向链表中的node节点
*/
private void unlink(DoubleNode node) {
//1.前空后空
//2.前不空后空
//3.前空后不空
//4.前不空后不空
DoubleNode prev = node.prev;
DoubleNode successor = node.next;
//先处理前半部分
if (prev == null) {
head = successor;
} else {
prev.next = successor;
node.prev = null;
}
//处理后半部分
if (successor == null) {
tail = prev;
} else {
successor.prev = prev;
node.next = null;
}
size--;
}
public void removeIndex(int index):删除索引为index的节点
/**
* 删除索引为index的节点
*/
public void removeIndex(int index) {
if (index < 0 || index >= size) {
System.err.println("remove index illegal!");
return;
}
DoubleNode cur = node(index);
unlink(cur);
}
public void removeFirst():删除头节点
/**
* 删头
*/
public void removeFirst() {
removeIndex(0);
}
public void removeLast():删除尾节点
/**
* 删尾
*/
public void removeLast() {
removeIndex(size - 1);
}
public void removeValueOnce(int val):删除第一个值为val的节点
/**
* 删除第一个值为val的节点
*/
public void removeValueOnce(int val) {
for (DoubleNode x = head; x != null; x = x.next) {
if (x.val == val) {
unlink(x);
break;
}
}
}
public void removeAllValue(int val):删除全部值为val的节点
/**
* 删除全部值为val的节点
*/
public void removeAllValue(int val) {
for (DoubleNode x = head; x != null; ) {
if (x.val == val) {
DoubleNode successor = x.next;
unlink(x);
x = successor;
} else {
x = x.next;
}
}
}
如有Bug,望指正,感谢浏览💖