链表
一个节点连接下一个节点构成一个链表。逻辑上是一个线性结构,实际上是由多个不同地址的空间通过指针连接起来。插入和删除速度快、随机访问速度慢。链表的几种实现:单向链表、双向链表、环形链表。
实现接口
public class DoublyLinkedListSentinel implements Iterable<Integer>
目的:实现遍历,可以使用增强for循环遍历节点
节点结构
通过定义一个内部类Node实现
单向链表的Node类
private static class Node{
int value;
Node next;
// 使用内部类Node,目的:对外隐藏实现细节(没必要让类的使用者关心Node结构),
// 定义为 static 内部类,是因为 Node 不需要与 SinglyLinkedList 实例相关,
// 多个 SinglyLinkedList实例能共用 Node 类定义,即
public Node(int value,Node next){
this.value = value;
this.next = next;
}
}
双向链表的Node类
static class Node {
Node prev;
int value;
Node next;
public Node(Node prev, int value, Node next) {
this.prev = prev;
this.value = value;
this.next = next;
}
}
单向链表
-
节点.next指向下一个节点
-
添加元素
-
删除节点
- 直接通过覆盖节点来实现
- p1.next= p1.next.next
java实现
不带头结点
package linklist;
import java.util.Iterator;
/**
* 单向链表
* @author hly
* @version 1.0
*/
public class SinglyLinkList implements Iterable<Integer>{
private Node head;// 头结点
// 静态内部类Node
private static class Node{
int value;
Node next;
// 使用内部类Node,目的:对外隐藏实现细节(没必要让类的使用者关心Node结构),
// 定义为 static 内部类,是因为 Node 不需要与 SinglyLinkedList 实例相关,
// 多个 SinglyLinkedList实例能共用 Node 类定义,即
public Node(int value,Node next){
this.value = value;
this.next = next;
}
}
/**
* 头插法
* @param value
*/
public void addFirst(int value){
this.head = new Node(value,this.head);
}
// while遍历
public void loopByWhile(){
Node curr = this.head;
while (curr!=null){
// 实现一些功能
System.out.println(curr.value);
curr = curr.next;
}
}
// for遍历
public void loopByFor(){
for (Node curr = this.head;curr!=null;curr=curr.next){
// 实现一些功能
System.out.println(curr.value);
}
}
// 迭代器遍历
// NodeIterator 要定义为非 static 内部类,显式定义一个内部类为了方便查看
// 因为SinglyLinkList遍历其实是Node内部类的遍历。
// 可以在实现iterator()方法时,直接返回一个匿名类
/*
return new Iterator<Integer>() {
Node curr = head;
@Override
public boolean hasNext() {
return curr!=null;
}
@Override
public Integer next() {
int value= curr.value;
curr = curr.next;
return value;
}
}
*/
// 是因为它与 SinglyLinkedList 实例相关,
// 是对某个 SinglyLinkedList 实例的迭代
private class NodeIterator implements Iterator<Integer>{
Node curr= head;
public boolean hasNext(){
return curr!=null;
}
public Integer next(){
int value = curr.value;
curr = curr.next;
return value;
}
}
/**
* 迭代器遍历,实现Iterable接口
* @return 返回一个iterator实现类,重写hasNext和next方法。
* hasNext判断迭代的条件
* next->对迭代元素进行的操作
*/
@Override
public Iterator<Integer> iterator() {
return new NodeIterator();
}
// 递归循环
public void loopByRecursion(){
recursion(this.head);
}
/**
* 递归函数
* @param curr
*/
private void recursion(Node curr) {
if (curr==null){
// 遍历到最后一个Node
return;
}
// 理解成中间件的调用执行顺序?
/*
先执行的,最后再执行?
相当于栈的调用,先进后出的调用顺序
*/
// 此次遍历做的事
System.out.println("我是先执行的value值"+curr.value);
recursion(curr.next);
// 后面做的事
System.out.println("我是后执行的value值"+curr.value);
}
/**
* 查找最后一个Node节点
* @return
*/
private Node findLast(){
if (this.head==null){
return null;
}
Node curr =this.head;
//找到最后一个Node.next为空的Node
while (curr.next!=null){
curr=curr.next;
}
return curr;
}
/**
* 尾部添加
* @param value 节点的value
*/
public void addLast(int value){
Node last = this.findLast();
if (last==null){
// 说明此时链表是null,相当于头插法
this.addFirst(value);
return;
}
// 将新结点插入
last.next = new Node(value,null);
}
/**
* 尾部添加多个
* 步骤:先将rest拼成一个长串,再添加到末尾
* @param first 第一个元素
* @param rest 不定长参数
*/
public void addLast(int first,int... rest){
Node sublist= new Node(first,null);
Node curr = sublist;
for (int value:rest){
curr.next = new Node(value,null);
curr = curr.next;
}
Node last = this.findLast();
if (last==null){
// 直接赋值给头结点
this.head = sublist;
return;
}
last.next = sublist;
}
/**
* 尾插法
* @param values value数组
*/
public void addLast(int[] values){
Node sublist= new Node(values[0],null);
Node curr = sublist;
for (int i = 1; i < values.length; i++) {
curr.next = new Node(values[i],null);
curr =curr.next;
}
Node last = this.findLast();
if (last==null){
// 直接赋值给头结点
this.head = sublist;
return;
}
last.next = sublist;
}
/**
* 根据索引获取
* @param index
* @return
*/
private Node findNode(int index){
int i = 0;
Node curr= this.head;
while (curr!=null){
if (i==index){
return curr;
}
curr = curr.next;
i++;
}
return null;
}
/**
* 封装异常类(index索引问题)
* 因为这个SinglyLinkList类查找时,没有index越界问题,只有Node为Null的情况
* @param index
* @return
*/
private IllegalArgumentException illegalIndex(int index){
return new
IllegalArgumentException(String.format("index [%d] 不合法%n",index));
}
/**
* 根据索引值获取value
* @param index
* @return
*/
public int get(int index){
Node node =this.findNode(index);
if (node!=null){
return node.value;
}
throw this.illegalIndex(index);
}
/**
* 根据Index插入数据
* @param index
* @param value
*/
public void insert(int index,int value){
if (index==0){
//说明头插法
this.addLast(value);
}
//查找应为上一个Node
Node node = this.findNode(index-1);
//判断
if (node==null){
throw illegalIndex(index);
}
// 赋值
node.next = new Node(value,node.next);
}
/**
* 删除
* @param index
* @return
*/
public void remove(int index){
if (index == 0){
if (this.head!=null) {
this.head = this.head.next;
return;
}else {
throw illegalIndex(index);
}
}
Node node = findNode(index-1);
if (node==null||node.next==null){
throw illegalIndex(index);
}
node.next = node.next.next;
}
}
带头结点
- 链表长度至少为1。
- 优点
- 添加元素时,不需要判断head是否为null
- 很多操作就没有必要判断head是否为null,减少不必要的操作
package linklist;
import java.util.Iterator;
/**
* 单向链表(带哨兵)
* 因为Head节点一直在,所以就没有判断head为空的必要
* 此时,很多操作就没有必要判断head是否为null,减少不必要的操作
* @author hly
* @version 1.0
*/
public class SinglyLinkListSentinel implements Iterable<Integer>{
private Node head = new Node(Integer.MIN_VALUE,null);// 头结点
// 静态内部类Node
private static class Node{
int value;
Node next;
// 使用内部类Node,目的:对外隐藏实现细节(没必要让类的使用者关心Node结构),
// 定义为 static 内部类,是因为 Node 不需要与 SinglyLinkedList 实例相关,
// 多个 SinglyLinkedList实例能共用 Node 类定义,即
public Node(int value,Node next){
this.value = value;
this.next = next;
}
}
/**
* 头插法
* @param value
*/
public void addFirst(int value){
this.head.next = new Node(value,this.head.next);
}
// while遍历
public void loopByWhile(){
Node curr = this.head.next;
while (curr!=null){
// 实现一些功能
System.out.println(curr.value);
curr = curr.next;
}
}
// for遍历
public void loopByFor(){
for (Node curr = this.head.next;curr!=null;curr=curr.next){
// 实现一些功能
System.out.println(curr.value);
}
}
// 迭代器遍历
// NodeIterator 要定义为非 static 内部类,显式定义一个内部类为了方便查看
// 因为SinglyLinkList遍历其实是Node内部类的遍历。
// 可以在实现iterator()方法时,直接返回一个匿名类
/*
return new Iterator<Integer>() {
Node curr = head;
@Override
public boolean hasNext() {
return curr!=null;
}
@Override
public Integer next() {
int value= curr.value;
curr = curr.next;
return value;
}
}
*/
// 是因为它与 SinglyLinkedList 实例相关,
// 是对某个 SinglyLinkedList 实例的迭代
private class NodeIterator implements Iterator<Integer>{
Node curr= head.next;
public boolean hasNext(){
return curr!=null;
}
public Integer next(){
int value = curr.value;
curr = curr.next;
return value;
}
}
/**
* 迭代器遍历,实现Iterable接口
* @return 返回一个iterator实现类,重写hasNext和next方法。
* hasNext判断迭代的条件
* next->对迭代元素进行的操作
*/
@Override
public Iterator<Integer> iterator() {
return new NodeIterator();
}
// 递归循环
public void loopByRecursion(){
recursion(this.head.next);
}
/**
* 递归函数
* @param curr
*/
private void recursion(Node curr) {
if (curr==null){
// 遍历到最后一个Node
return;
}
// 理解成中间件的调用执行顺序?
/*
先执行的,最后再执行?
相当于栈的调用,先进后出的调用顺序
*/
// 此次遍历做的事
System.out.println("我是先执行的value值"+curr.value);
recursion(curr.next);
// 后面做的事
System.out.println("我是后执行的value值"+curr.value);
}
/**
* 查找最后一个Node节点
* @return
*/
private Node findLast(){
Node curr =this.head;
//找到最后一个Node.next为空的Node
while (curr.next!=null){
curr=curr.next;
}
return curr;
}
/**
* 尾部添加
* @param value 节点的value
*/
public void addLast(int value){
Node last = this.findLast();
// 将新结点插入
last.next = new Node(value,null);
}
/**
* 尾部添加多个
* 步骤:先将rest拼成一个长串,再添加到末尾
* @param first 第一个元素
* @param rest 不定长参数
*/
public void addLast(int first,int... rest){
Node sublist= new Node(first,null);
Node curr = sublist;
for (int value:rest){
curr.next = new Node(value,null);
curr = curr.next;
}
Node last = this.findLast();
last.next = sublist;
}
/**
* 尾插法
* @param values value数组
*/
public void addLast(int[] values){
Node sublist= new Node(values[0],null);
Node curr = sublist;
for (int i = 1; i < values.length; i++) {
curr.next = new Node(values[i],null);
curr =curr.next;
}
Node last = this.findLast();
last.next = sublist;
}
/**
* 根据索引获取
* @param index
* @return
*/
private Node findNode(int index){
// 代表的是head节点,当插入的index为0时,
// 即FindNode()接收到是参数是-1,即为head节点
int i = -1;
Node curr= this.head;
while (curr!=null){
if (i==index){
return curr;
}
curr = curr.next;
i++;
}
return null;
}
/**
* 封装异常类(index索引问题)
* 因为这个SinglyLinkList类查找时,没有index越界问题,只有Node为Null的情况
* @param index
* @return
*/
private IllegalArgumentException illegalIndex(int index){
return new
IllegalArgumentException(String.format("index [%d] 不合法%n",index));
}
/**
* 根据索引值获取value
* @param index
* @return
*/
public int get(int index){
Node node =this.findNode(index);
if (node!=null){
return node.value;
}
throw this.illegalIndex(index);
}
/**
* 根据Index插入数据
* index的取值为[-1,.] -1代表head节点
* @param index
* @param value
*/
public void insert(int index,int value){
//查找应为上一个Node
// 传入0时,返回的是哨兵
Node node = this.findNode(index-1);
//判断
if (node==null){
throw illegalIndex(index);
}
// 赋值
node.next = new Node(value,node.next);
}
/**
* 删除
* @param index
* @return
*/
public void remove(int index){
Node node = findNode(index-1);
if (node==null||node.next==null){
throw illegalIndex(index);
}
node.next = node.next.next;
}
}
双向链表
- head、tail两个节点
- 链表为空时
- head.next= tail;tail.prev=head;
- 优点
- 可以双向遍历:由于每个节点都有指向前一个节点的指针,因此可以从任意一个节点开始,向前或向后遍历整个链表。这使得双向链表在某些场景下更加方便,比如需要逆向遍历链表或者需要双向查找某个节点。
- 删除和插入操作更高效:在单向链表中,如果要删除或插入某个节点,需要知道其前驱节点的位置,而双向链表通过指针的前后关系,不需要再查找前驱节点。删除和插入操作可以在常数时间内完成。
- 灵活性更强:双向链表相比单向链表可以更灵活地操作节点。例如,可以通过前后指针直接交换两个相邻节点的位置,而无需修改其他节点的指针。
- 更容易实现某些功能:由于双向链表具有双向遍历的特性,因此在实现一些功能时更加便捷。例如,实现LRU(最近最少使用)缓存算法时,可以使用双向链表来快速删除最久未使用的节点。
- 添加与删除跟单向链表类似,多一步骤:前驱指针的指向。
java实现
package linklist;
import java.util.Iterator;
/**双向链表(带哨兵)
* @author hly
* @version 1.0
*/
public class DoublyLinkedListSentinel implements Iterable<Integer>{
// 头结点
private final Node head;
// 尾节点
private final Node tail;
//Node内部类
private static class Node{
//上一个节点
Node prev;
int value;
// 下一个节点
Node next;
public Node(Node prev, int value, Node next) {
this.prev = prev;
this.value = value;
this.next = next;
}
}
//初始化
public DoublyLinkedListSentinel(){
head = new Node(null,666,null);
tail = new Node(null,888,null);
head.next=tail;
tail.prev=head;
}
/**
* 根据Index获取节点
* @param index
* @return
*/
private Node findNode(int index){
int i = -1;
for (Node p = this.head; p!=tail; p= p.next,i++) {
if (i==index){
return p;
}
}
return null;
}
/**
* 插入第一个位置
* @param value
*/
public void addFirst(int value){
this.insert(0,value);
}
/**
* 末尾插入元素
* @param value
*/
public void addLast(int value){
Node prev = tail.prev;
Node added = new Node(prev,value,tail);
prev.next=added;
tail.prev=added;
}
/**
* 删除第一个元素
*/
public void removeFirst(){
this.remove(0);
}
public void removeLast(){
Node removed = tail.prev;
if (removed==head){
//null
throw illegalIndex(0);
}
Node prev = removed.prev;
prev.next=tail;
tail.prev=prev;
}
/**
* 根据Index删除Node
* @param index
*/
public void remove(int index) {
Node prev = this.findNode(index - 1);
if (prev==null){
throw illegalIndex(index);
}
Node removed= prev.next;
if (removed==tail){
// 此时双向链表没有任何一个节点(除头尾指针外)
throw illegalIndex(index);
}
/**
* 简易版
* Node next = removed.next;
* prev.next = next;
* next.prev = prev;
*/
prev.next=removed.next;
removed.next.prev=prev;
}
/**
* 根据Index添加Node
* @param index
* @param value
*/
public void insert(int index, int value) {
Node prev = this.findNode(index - 1);
if (prev==null){
throw illegalIndex(index);
}
Node next = prev.next;
Node inserted = new Node(prev,value,next);
prev.next =inserted;
// 将插入前的那个位置Nodeprev指针指向上一个
next.prev=inserted;
}
private IllegalArgumentException illegalIndex(int index) {
return new IllegalArgumentException(String.format("index[%d]不合法%n",index));
}
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
Node p =head.next;
@Override
public boolean hasNext() {
return p.next!=tail;
}
@Override
public Integer next() {
int value = p.value;
p = p.next;
return value;
}
};
}
}
环形链表
- 链表前驱和后驱指针互相指向形成闭环,头->尾,尾->头。
- 可以循环遍历,这种特性在某些场景下非常有用,例如模拟循环队列、轮询等
- 快速判断环:环形链表具有环形结构,使用快慢指针法可以快速判断链表是否存在环。快慢指针以不同的速度遍历链表,如果存在环,两个指针最终会相遇;如果不存在环,则快指针会先到达链表末尾。
- 节省存储空间:在某些情况下,环形链表可以节省存储空间。例如,在循环队列中,使用环形链表可以避免对队列大小进行动态调整和数据迁移。
- 实现特定算法和数据结构:环形链表在实现某些特定算法和数据结构时非常方便。例如,约瑟夫问题(Josephus problem)可以使用环形链表解决,也可以使用环形链表实现循环缓冲区等。
java实现
package linklist;
import java.util.Iterator;
/**
* 环形链表
* @author hly
* @version 1.0
*/
public class CircularLinkedList implements Iterable<Integer>{
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
Node pre = sentinel.next;
@Override
public boolean hasNext() {
return pre!=sentinel;
}
@Override
public Integer next() {
int value = pre.value;
pre=pre.next;
return value;
}
};
}
static class Node {
Node prev;
int value;
Node next;
public Node(Node prev, int value, Node next) {
this.prev = prev;
this.value = value;
this.next = next;
}
}
// 哨兵
private final Node sentinel =new Node(null,-1,null);
public CircularLinkedList() {
sentinel.next = sentinel;
sentinel.prev = sentinel;
}
/**
* 添加到第一个元素
* @param value 待添加值
*/
public void addFirst(int value){
Node next =sentinel.next;
Node prev = sentinel;
Node added = new Node(prev,value,next);
prev.next= added;
next.prev=added;
}
/**
* 添加末尾
* @param value
*/
public void addLast(int value){
Node prev = sentinel.prev;
Node next = sentinel;
Node added = new Node(prev,value,next);
next.prev = added;
prev.next = added;
}
/**
* 删除第一个元素
*/
public void removeFirst(){
Node removed = sentinel.next;
if (removed==sentinel){
throw illegalIndex(0);
}
// Node a = sentinel;
Node node = removed.next;
// a.next =node;
// node.prev=a;
node.prev = removed.prev;
removed.prev.next =node;
}
/**
* 删除末尾元素
*/
public void removeLast(){
Node removed = sentinel.prev;
if (removed == sentinel){
throw new IllegalArgumentException("非法");
}
Node node = removed.next;
removed.prev.next = node;
node.prev = removed.prev;
}
/**
* 根据值删除节点
* 假定 value 在链表中作为 key, 有唯一性
* @param value
*/
public void removeByValue(int value){
Node removed = this.findNodeByValue(value);
if (removed ==null){
throw new IllegalArgumentException("value值不存在!");
}
Node prev = removed.prev;
prev.next = removed.next;
removed.next.prev = prev;
}
private Node findNodeByValue(int value) {
Node node = this.sentinel.next;
while (node!=sentinel){
if (node.value==value){
return node;
}
node = node.next;
}
return null;
}
public IllegalArgumentException illegalIndex(int index){
return new IllegalArgumentException(String.format("inex [%d] 不合法%n",index));
}
}