先声明:我比较懒,所以没有画图,不理解代码的请自行百度或者查看相关书籍🤭
链表是最常用的一种数据结构,作为线性表的一种,与数组相比,链表在插入修改操作多的环境中有着非常大的优势。下面我们用Java实现一个完整的链表。
链表是有多个节点构成的,每个节点应当包含 数据域(用来存放数据)和 next指针(Java中是下一个节点的引用)两部分。
同时需要提供以下操作链表的方法:
- 初始化链表
- 判断链表是否为空
- 清空链表
- 获取第i个位置的元素
- 根据对应的值查找元素的位置
- 插入新元素(头插法和尾插法)
- 删除元素
- 获取链表长度
分析完毕,下面开始实现~
public class LinkList {
}
节点
首先我们需要实现链表中用来存储数据的节点结构。我们使用一个静态内部类来表示节点,假设我们实现的链表是用来存取整形的数据。
代码实现如下:
/**
* 链表的节点
*/
public static class Node{
/**
* 节点存储的值
*/
public int value;
/**
* 下一个节点
*/
public Node next;
}
操作
-
初始化链表
要想后边使用链表,我们必须拥有一个指向链表头部的引用,否则我们无法对其进行任何操作。这里我又添加了一个尾节点,为的是后面尾插法方便。我们再链表类的构造方法中调用初始化方法,主要是用来初始化头节点和尾节点。其中头节点的next指向链表的第一个节点,尾节点指向链表的最后一个节点。
代码如下:
// 用来存储链表的头部 private Node head; // 用来存储链表的尾部 private Node tail; public LinkList(){ // 初始化链表 initList(); } /** * 初始化链表 */ public void initList(){ // 创建头节点 链表的第一个节点的引用将保存在head.next中 head = new Node(); head.value = 0; head.next = null; // 初始时没有元素 尾节点指向头节点 tail = head; }
-
判断链表是否为空
这个实现就比较简单了,如果链表为空,那么我们头节点的next引用一定时null,所以代码实现如下:
/** * 判断链表是否为空 * @return */ public boolean isEmpty(){ return head.next == null; }
-
清空链表
这个操作再C/C++中比较麻烦,但是用Java实现确实非常简单,我们仅仅只需要把head节点next引用改为null即可,因为可以获得这个链表的引用没有,再GC的可达性分析中,这个链表被认为时不可达的,在GC时链表就会作为垃圾被从内存中清除掉。所以,我们要做的仅仅是把head节点的next引用置为null。
/** * 清空链表 */ public void clear(){ head.next = null; }
-
获取链表第i个位置的值
终于需要遍历链表了😂,我们需要一个计数器,用来记录我们遍历的位置,这一点与数组相比就比较难受了,数组因为在内存中是连续存储的,因此可以直接使用数组头部的地址和每个元素的大小直接计算出需要查找元素的位置,而链表由于在内存中不是连续存储的,所以无法通过计算得出,只能是遍历链表。因此在查询比较多的场景中,数组更占优势。
/** * 获取第i个节点的元素 * @param i * @return */ public int getNode(int i){ // 获取第一个节点 Node node = head.next; // 记录遍历到的索引 int index = 0; // 遍历 while(node != null){ // 找到对应的索引后返回节点存储的值 if(index == i){ return node.value; } // 指向下一个节点 node = node.next; index++; } // 遍历完毕后还没有找到,说明索引越界 返回-1 return -1; }
-
获取对应值的索引
这个实现与上面的思路就比较类似了,不在进行赘述,直接实现
/** * 获取对应值的索引 * @param value * @return */ public int getIndex(int value){ // 获取第一个节点 Node node = head.next; // 记录索引 int index = 0; while(node != null){ // 将遍历到节点的值与要查找的值比较 if(node.value == value){ return index; } node = node.next; index++; } return -1; }
-
插入元素
插入元素分为两种,一种是尾插法,一种是头插法。相比而言,尾插法比较简单,我们先实现尾插法。
/** * 尾部插入 */ public void tailInsert(int value){ // 构造节点 Node node = new Node(); node.value = value; node.next = null; if(isEmpty()){ // 链表为空时,此时head节点指向为null // node会作为第一个元素,因此需要将head的next指向node head.next = node; }else{ // 链表不为空时,head已经指向链表的第一个元素了,只需要将尾节点的next指向新插入的节点即可 tail.next = node; } // 更新尾节点 tail = node; }
头插法实现起来稍微复杂,因为head的next已经指向了链表的第一个节点(链表不为空时),因为是头插法,新插入的节点将会作为新的头节点,因此,需要更新head的next指向新插入的节点,同时需要将新插入的节点的next指向原来的第一个节点,代码实现如下:
/** * 头部插入法 */ public void headInsert(int value){ // 构造节点 Node node = new Node(); node.value = value; node.next = null; if(isEmpty()){ // 链表为空时比较简单,只需要将头节点的next指向新插入的节点 // 注意:需要更新尾节点 head.next = node; tail = node; }else{ // 链表不为空时,顺序不能乱,不需要更新尾节点 // 先将新节点的next指向原来的第一个节点 node.next = head.next; // 再将头节点的next指向新插入的节点 head.next = node; } }
-
删除第i个元素
这个要考虑的情况比较复杂,我直接写在注释上了,结合注释看应该更加好
/** * 删除 * @param i 要删除的索引 * @return 删除元素的值 */ public int delete(int i){ // 链表为空时返回-1 if(isEmpty())return -1; // 遍历的索引 这次从-1开始 int index = -1; // 这次使用的时head 不是head的next,因为下面的遍历包含删除第一个节点的情况 Node node = head; while(node != null){ // 遍历到删除目标节点的前一个节点 if( index == (i-1) ){ // 获取目标节点 Node target = node.next; if(target == null){ // 如果要删除的节点刚好越界 返回-1 return -1; } // 将目标节点上个节点的next引用直接修改为目标节点的next指向的节点 // 可以理解为用下下个节点覆盖下个节点 node.next = node.next.next; if(target.next == null){ // 如果删除的节点是尾节点 需要更新尾节点 tail = node; } // 成功删除后返回删除的值 return target.value; } node = node.next; index++; } return -1; }
-
链表长度
这个实现的方式比较多,可以在链表类中添加一个记录长度的字段len,在添加元素时len++,在删除元素时len--,获取链表长度的方法只需要返回这个字段的值即可。还有一种方法是遍历链表时计数,这里实现的时第二种方法:
/** * 返回链表长度 * @return */ public int length(){ int count = 0; Node node = head.next; while(node != null){ count++; node = node.next; } return count; }
测试使用
为了方便测试,我在链表类中添加了一个展示的方法,可以遍历输出链表的所有元素,实现如下:
public void display(){
if(isEmpty())System.out.println("链表为空");
Node node = head.next;
while(node != null){
System.out.print(node.value + "-");
node = node.next;
}
System.out.println();
}
下面使用我们自己实现的链表来完成测试
public class Test {
public static void main(String[] args) {
LinkList linkList = new LinkList();
System.out.println("===初始化===");
linkList.display();
System.out.println("===尾插2 头插1 尾插3 ===");
linkList.tailInsert(2);
linkList.headInsert(1);
linkList.tailInsert(3);
linkList.display();
System.out.println("===获取值为1的索引===");
System.out.println(linkList.getIndex(1));
System.out.println("===获取索引为1的值===");
System.out.println(linkList.getNode(1));
System.out.println("===删除索引为1的节点==");
linkList.delete(1);
linkList.display();
System.out.println("===查看链表长度===");
System.out.println(linkList.length());
System.out.println("===清空链表===");
linkList.clear();
linkList.display();
}
}
输出如下:
===初始化===
链表为空
===尾插2 头插1 尾插3 ===
1-2-3-
===获取值为1的索引===
0
===获取索引为1的值===
2
===删除索引为1的节点==
1-3-
===查看链表长度===
2
===清空链表===
链表为空
这里只是实现了链表的基本操作,其他的诸如在指定位置插入元素等方法可以自行尝试实现!
下面是完整的链表类代码:
public class LinkList {
/**
* 链表的节点
*/
public static class Node{
/**
* 节点存储的值
*/
public int value;
/**
* 下一个节点
*/
public Node next;
}
// 用来存储链表的头部
private Node head;
// 用来存储链表的尾部
private Node tail;
public LinkList(){
// 初始化链表
initList();
}
/**
* 初始化链表
*/
public void initList(){
// 创建头节点 链表的第一个节点的引用将保存在head.next中
head = new Node();
head.value = 0;
head.next = null;
// 初始时没有元素 尾节点指向头节点
tail = head;
}
/**
* 判断链表是否为空
* @return
*/
public boolean isEmpty(){
return head.next == null;
}
/**
* 清空链表
*/
public void clear(){
head.next = null;
}
/**
* 获取第i个节点的元素
* @param i
* @return
*/
public int getNode(int i){
// 获取第一个节点
Node node = head.next;
// 记录遍历到的索引
int index = 0;
// 遍历
while(node != null){
// 找到对应的索引后返回节点存储的值
if(index == i){
return node.value;
}
// 指向下一个节点
node = node.next;
index++;
}
// 遍历完毕后还没有找到,说明索引越界 返回-1
return -1;
}
/**
* 获取对应值的索引
* @param value
* @return
*/
public int getIndex(int value){
// 获取第一个节点
Node node = head.next;
// 记录索引
int index = 0;
while(node != null){
// 将遍历到节点的值与要查找的值比较
if(node.value == value){
return index;
}
node = node.next;
index++;
}
return -1;
}
/**
* 尾部插入
*/
public void tailInsert(int value){
// 构造节点
Node node = new Node();
node.value = value;
node.next = null;
if(isEmpty()){
// 链表为空时,此时head节点指向为null
// node会作为第一个元素,因此需要将head的next指向node
head.next = node;
}else{
// 链表不为空时,head已经指向链表的第一个元素了,只需要将尾节点的next指向新插入的节点即可
tail.next = node;
}
// 更新尾节点
tail = node;
}
/**
* 头部插入法
*/
public void headInsert(int value){
// 构造节点
Node node = new Node();
node.value = value;
node.next = null;
if(isEmpty()){
// 链表为空时比较简单,只需要将头节点的next指向新插入的节点
// 注意:需要更新尾节点
head.next = node;
tail = node;
}else{
// 链表不为空时,顺序不能乱,不需要更新尾节点
// 先将新节点的next指向原来的第一个节点
node.next = head.next;
// 再将头节点的next指向新插入的节点
head.next = node;
}
}
/**
* 删除
* @param i 要删除的索引
* @return 删除元素的值
*/
public int delete(int i){
// 链表为空时返回-1
if(isEmpty())return -1;
// 遍历的索引 这次从-1开始
int index = -1;
// 这次使用的时head 不是head的next,因为下面的遍历包含删除第一个节点的情况
Node node = head;
while(node != null){
// 遍历到删除目标节点的前一个节点
if( index == (i-1) ){
// 获取目标节点
Node target = node.next;
if(target == null){
// 如果要删除的节点刚好越界 返回-1
return -1;
}
// 将目标节点上个节点的next引用直接修改为目标节点的next指向的节点
// 可以理解为用下下个节点覆盖下个节点
node.next = node.next.next;
if(target.next == null){
// 如果删除的节点是尾节点 需要更新尾节点
tail = node;
}
// 成功删除后返回删除的值
return target.value;
}
node = node.next;
index++;
}
return -1;
}
/**
* 返回链表长度
* @return
*/
public int length(){
int count = 0;
Node node = head.next;
while(node != null){
count++;
node = node.next;
}
return count;
}
public void display(){
if(isEmpty())System.out.println("链表为空");
Node node = head.next;
while(node != null){
System.out.print(node.value + "-");
node = node.next;
}
System.out.println();
}
}