目录
1、基本概念和访问性能
链表和数组相似,都是线性存储的。链表中每一个元素都是指向下一个元素的,在内存中,地址空间是不连续的。
链表有单向链表、双向链表和循环链表三种
单向链表:每一个元素只知道下一个元素是谁,不知道上一个元素
双向链表:每一个节点元素都知道自己的上一个元素和下一个元素
循环链表:通常链表的尾结点的 next 指针都是 null,但是对于循环链表,尾结点的 next 指针指向头结点
随机访问性能:O(n)
插入或者删除性能:找到插入点O(n) + 插入操作O(1) = O(n)
2、单项链表的基本操作(没有哨兵)
2.1、单向链表的定义
根据单向链表的相关定义,在单项链表由每一个独立的节点数据组成,可以定义单项链表的类如下,定义一个存储当前节点数据的 Node 节点和链表的头结点。
没有哨兵表示链表的头结点是真实有有效的数据。
/**
* 单向链表
*
* @author zjj_admin
*/
public class SinglyLinkedList {
/**
* 链表头指针,这里头结点是真实有效的
*/
private Node head = null;
/**
* 链表中的节点,链表和节点是组合的关系,所以做成内部内比较合适。
* <p>
* static 关键字为什么要加?什么时候应该使用 static ?
*/
private static class Node {
/**
* 节点数据
*/
int value;
/**
* 当前节点的下一个指针
*/
Node next;
public Node(int value, Node next) {
this.value = value;
this.next = next;
}
}
}
思考:
1:为什么在定义 Node 要是用内部类,而不是用外部类?
因为我们操作的对象时链表不是节点,所以说一般不要让使用者知道我们链表的内部实现效果。
2:为什么要是用 static 修饰成静态内部类?这个后面解答,答案在单链表遍历部分里面
2.2、头部添加数据
添加数据思路:
a:当空链表时,直接将头结点指向插入数据即可
b:当不是空链表时
-
使用新添加数据创建一个新的节点
-
新节点的 next 指针指向 head
-
让 head 节点指向新节点即可
/**
* 单向链表
*
* @author zjj_admin
*/
public class SinglyLinkedList {
/**
* 在链表的头部插入一个数据
*
* @param value 待插入数据
*/
public void addFirstInitial(int value) {
//当head == null 时【链表为空】,可以被优化
if (head == null) {
head = new Node(value, null);
} else {
//当链表不为空时,让当前节点的 next 指针指向 head 节点即可,然后再将 head 指向当前节点,具体实现代码如下
Node curr = new Node(value, null);
curr.next = head;
head = curr;
}
}
}
可以将上面的添加代码进行简化
/**
* 在链表的头部插入一个数据
*
* @param value 待插入数据
*/
public void addFirst(int value) {
//可以将上面的代码进行简化
head = new Node(value, head);
}
2.3、单链表遍历
使用 while 循环进行遍历,使用 Consumer 函数式接口
/**
* 遍历链表,使用函数式接口 Consumer
*/
public void loopWhile(Consumer<Integer> consumer) {
Node p = head;
//当前节点不为 null 就一直循环
while (p != null) {
// accept 里面传入相关的参数
consumer.accept(p.value);
p = p.next;
}
}
测试代码
SinglyLinkedList linkedList = new SinglyLinkedList();
linkedList.addFirst(1);
linkedList.addFirst(2);
linkedList.addFirst(3);
linkedList.addFirst(4);
//遍历数据
linkedList.loopWhile((value) -> {
System.out.println(value);
});
使用 for 循环进行遍历
/**
* 遍历链表,使用函数式接口 Consumer
*/
public void loopFor(Consumer<Integer> consumer) {
//使用基于 for 循环遍历
for (Node p = head; p != null; p = p.next) {
consumer.accept(p.value);
}
}
测试方式和 while 循环遍历相同
使用迭代器 Iterable 遍历
实现代码,需要实现 Iterable 接口并且重写 iterator 方法
/**
* 单向链表
*
* @author zjj_admin
*/
public class SinglyLinkedList implements Iterable<Integer> {
private Node head = null;
/**
* 基于迭代器进行循环
*
* @return
*/
@Override
public Iterator<Integer> iterator() {
/**
* 匿名内部类
*/
return new Iterator<Integer>() {
Node p = head;
/**
* 是否存在下一个元素
* @return
*/
@Override
public boolean hasNext() {
return p != null;
}
/**
* 返回当前数据,并指向下一个元素
* @return
*/
@Override
public Integer next() {
int v = p.value;
p = p.next;
return v;
}
};
}
}
这里回答一下为什么 Node 节点要声明成静态内部类。
观察上面的 iterator() 方法,实际上就是一个匿名内部类,转化成内部类的方式代码如下:
/**
* 基于迭代器进行循环
*
* @return
*/
@Override
public Iterator<Integer> iterator() {
/**
* 匿名内部类
*/
return new NodeIterator();
}
/**
* 匿名内部类的完整写法
* 这里不能添加 static ,因为外 NodeIterator 里面引用了外部类的 head 成员变量
*/
private class NodeIterator implements Iterator<Integer> {
Node p = head;
/**
* 是否存在下一个元素
* @return
*/
@Override
public boolean hasNext() {
return p != null;
}
/**
* 返回当前数据,并指向下一个元素
* @return
*/
@Override
public Integer next() {
int v = p.value;
p = p.next;
return v;
}
}
因为在 NodeIterator 类里面用到了外部类的成员变量 head,所以说 NodeIterator 类一定不能使用 static关键字。
static 关键字为什么要加?什么时候应该使用 static ?
-
当内部内中没有引用外部类的成员变量时,就推荐加 static。
-
当内部内中有引用外部类的成员变量时,就一定不能加 static。
2.4、获取链表长度和获取链表所有数据
在获取链表数据时需要将数据存放在一个数组中,所以需要先求出链表的长度大小,实现代码如下。
/**
* 获取链表的长度
*
* @return
*/
public int size() {
int size = 0;
if (head == null) {
return size;
}
Node p = head;
while (p != null) {
size++;
p = p.next;
}
return size;
}
/**
* 获取链表中的所有数据
*
* @return 数据数组
*/
public int[] values() {
//获取链表长度
int size = size();
int[] nums = new int[size];
//索引从 0 开始
int i = 0;
Node p = head;
while (i < size) {
nums[i] = p.value;
p = p.next;
i++;
}
return nums;
}
2.5、尾部添加数据
退步添加数据实现思路
1:当链表为空时,按照头部插入的方式添加即可
2:当链表不为空时,需要插在最后一个节点的后面,所以需要先找到尾部节点。
/**
* 添加数据到尾结点
*
* @param value
*/
public void addLast(int value) {
Node last = findLast();
if (last == null) {
//没有数据,添加到头部即可
addFirst(value);
return;
}
//将当前数据节点存放到 last 的 next 指针
last.next = new Node(value, null);
}
/**
* 找到当前链表的最后一个节点
*
* @return
*/
private Node findLast() {
//当头结点为空时就直接返回 null
if (head == null) {
return null;
}
Node p = head;
//依次遍历,当当前节点 的 next 指针为 null 时,就表示是最后一个节点了
while (p.next != null) {
p = p.next;
}
return p;
}
2.6、根据索引(index)获取相应的节点数据
/**
* 获取索引为 index 对应节点的数据
*
* @param index
* @return
*/
public int get(int index) {
Node node = getNode(index);
if (null == node) {
throw new IndexOutOfBoundsException("index:" + index + " 越界了");
}
return node.value;
}
/**
* 获取索引为 index 对应节点
*
* @param index
* @return
*/
private Node getNode(int index) {
int i = 0;
Node p = head;
while (p != null) {
//只要当 i 和索引相等时就可以反悔了
if (i == index) {
return p;
}
p = p.next;
i++;
}
return null;
}
2.9、带哨兵的单向链表的相关操作
2.7、向单链表固定的索引位置插入数据(重点☆☆☆☆☆)
在固定的位置插入数据的步骤:
1:找到插入位置的上一个节点
2:在上一个节点后面插入新数据即可
实现代码如下:
/**
* 在链表的指定位置插入数据,详细代码
*
* @param index 位置索引
* @param value 插入数据
*/
public void addInitial(int index, int value) {
if (index == 0) {
//添加在头部
addFirst(value);
return;
}
//获取上一个节点
Node prev = getNode(index - 1);
if (prev == null) {
throw new IndexOutOfBoundsException("index:" + index + " 越界了");
}
//当前需要插入的节点
Node curr = new Node(value, null);
//让当前节点的 next 指针指向 prev 的 next
curr.next = prev.next;
//再让 上一个节点的 next 指针指向 curr
prev.next = curr;
}
简化代码:
/**
* 在链表的指定位置插入数据,精简代码
*
* @param index 位置索引
* @param value 插入数据
*/
public void add(int index, int value) {
if (index == 0) {
//添加在头部
addFirst(value);
return;
}
//获取上一个节点
Node prev = getNode(index - 1);
if (prev == null) {
throw new IndexOutOfBoundsException("index:" + index + " 越界了");
}
//当前需要插入的节点
prev.next = new Node(value, prev.next);
}
2.8、指定位置删除节点(重点☆☆☆☆☆)
/**
* 删除第一个节点
*
* @return 被删除的索引节点数据
*/
public int removeFirst() {
if (head == null) {
throw new IndexOutOfBoundsException("没有数据了...");
}
int v = head.value;
//让头结点的 next 节点成为新的头节点即可
head = head.next;
return v;
}
/**
* 根据索引数据
*
* @param index 删除索引
* @return 被删除的索引节点数据
*/
public int remove(int index) {
if (index == 0) {
//深处第一个元素
return removeFirst();
}
//获取被删除节点的上一个节点
Node prev = getNode(index - 1);
if (prev == null) {
throw new IndexOutOfBoundsException("index " + index + " 越界了...");
}
Node removed = prev.next;
if (removed == null) {
throw new IndexOutOfBoundsException("index " + index + " 越界了...");
}
int v = removed.value;
//只需要将被删除节点的上一个节点的 next 指针指向被删除节点的 next 指针就可以了
prev.next = removed.next;
return v;
}
3、有哨兵节点的单项链表
带哨兵的单项链表可以将链表的 head 节点设置为哨兵节点,哨兵节点一直存在,不存储真实的数据。
查看下列代码,是哨兵模式下的单向链表定义。
和没有哨兵模式相比,带哨兵的模式代码更加简化,很多时候不需要考虑头结点为空的情况了。
/**
* 单向链表
* 有哨兵节点,哨兵节点是 head 节点,head 节点后面的数据才是真正的数据
*
* @author zjj_admin
*/
public class SinglyLinkedListSentinel implements Iterable<Integer> {
/**
* 链表头指针,这里头结点是哨兵,没有含义
*/
private Node head = new Node(Integer.MIN_VALUE, null);
/**
* 在链表的头部插入一个数据,不需要考虑头结点为 null 的情况了
*
* @param value 待插入数据
*/
public void addFirstInitial(int value) {
//定义当前节点
Node curr = new Node(value, null);
//head 节点是哨兵节点
curr.next = head.next;
head.next = curr;
}
/**
* 遍历链表,使用函数式接口 Consumer
*/
public void loopWhile(Consumer<Integer> consumer) {
Node p = head.next;
//当前节点不为 null 就一直循环
while (p != null) {
// accept 里面传入相关的参数
consumer.accept(p.value);
p = p.next;
}
}
/**
* 遍历链表,使用函数式接口 Consumer
*/
public void loopFor(Consumer<Integer> consumer) {
//使用基于 for 循环遍历
for (Node p = head.next; p != null; p = p.next) {
consumer.accept(p.value);
}
}
/**
* 在链表的头部插入一个数据
*
* @param value 待插入数据
*/
public void addFirst(int value) {
//可以将上面的代码进行简化
head.next = new Node(value, head.next);
}
/**
* 添加数据到尾结点
*
* @param value
*/
public void addLast(int value) {
Node last = findLast();
//将当前数据节点存放到 last 的 next 指针
last.next = new Node(value, null);
}
/**
* 找到当前链表的最后一个节点
*
* @return
*/
private Node findLast() {
Node p = head;
//依次遍历,当当前节点 的 next 指针为 null 时,就表示是最后一个节点了
while (p.next != null) {
p = p.next;
}
return p;
}
/**
* 获取链表的长度
*
* @return
*/
public int size() {
int size = 0;
Node p = head.next;
while (p != null) {
size++;
p = p.next;
}
return size;
}
/**
* 获取链表中的所有数据
*
* @return 数据数组
*/
public int[] values() {
//获取链表长度
int size = size();
int[] nums = new int[size];
//索引从 0 开始
int i = 0;
Node p = head.next;
while (i < size) {
nums[i] = p.value;
p = p.next;
i++;
}
return nums;
}
/**
* 获取索引为 index 对应节点的数据
*
* @param index
* @return
*/
public int get(int index) {
Node node = getNode(index);
if (null == node) {
throw new IndexOutOfBoundsException("index:" + index + " 越界了");
}
return node.value;
}
/**
* 获取索引为 index 对应节点
* 哨兵节点的索引为 -1,真实节点的索引从 0 开始
*
* @param index
* @return
*/
private Node getNode(int index) {
int i = -1;
Node p = head;
while (p != null) {
//只要当 i 和索引相等时就可以反悔了
if (i == index) {
return p;
}
p = p.next;
i++;
}
return null;
}
/**
* 在链表的指定位置插入数据,详细代码
*
* @param index 位置索引
* @param value 插入数据
*/
public void addInitial(int index, int value) {
//获取上一个节点
Node prev = getNode(index - 1);
if (prev == null) {
throw new IndexOutOfBoundsException("index:" + index + " 越界了");
}
//当前需要插入的节点
Node curr = new Node(value, null);
//让当前节点的 next 指针指向 prev 的 next
curr.next = prev.next;
prev.next = curr;
}
/**
* 在链表的指定位置插入数据,精简代码
*
* @param index 位置索引
* @param value 插入数据
*/
public void add(int index, int value) {
//获取上一个节点
Node prev = getNode(index - 1);
if (prev == null) {
throw new IndexOutOfBoundsException("index:" + index + " 越界了");
}
//当前需要插入的节点
prev.next = new Node(value, prev.next);
}
/**
* 删除第一个节点
*
* @return 被删除的索引节点数据
*/
public int removeFirst() {
if (head.next == null) {
throw new IndexOutOfBoundsException("没有数据了...");
}
int v = head.next.value;
head.next = head.next.next;
return v;
}
/**
* 根据索引数据
*
* @param index 删除索引
* @return 被删除的索引节点数据
*/
public int remove(int index) {
//获取被删除节点的上一个节点
Node prev = getNode(index - 1);
if (prev == null) {
throw new IndexOutOfBoundsException("index " + index + " 越界了...");
}
Node removed = prev.next;
if (removed == null) {
throw new IndexOutOfBoundsException("index " + index + " 越界了...");
}
int v = removed.value;
//只需要将被删除节点的上一个节点的 next 指针指向被删除节点的 next 指针就可以了
prev.next = removed.next;
return v;
}
/**
* 基于迭代器进行循环
*
* @return
*/
@Override
public Iterator<Integer> iterator() {
/**
* 匿名内部类
*/
return new Iterator<Integer>() {
Node p = head.next;
/**
* 是否存在下一个元素
* @return
*/
@Override
public boolean hasNext() {
return p != null;
}
/**
* 返回当前数据,并指向下一个元素
* @return
*/
@Override
public Integer next() {
int v = p.value;
p = p.next;
return v;
}
};
}
/**
* 链表中的节点,链表和节点是组合的关系,所以做成内部内比较合适。
*/
private static class Node {
/**
* 节点数据
*/
int value;
/**
* 当前节点的下一个指针
*/
Node next;
public Node(int value, Node next) {
this.value = value;
this.next = next;
}
}
}