目录
前言
在上一篇中我们介绍了顺序表。在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后 搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java 集合中又引入了LinkedList,即链表结构
一.链表
1.1链表的概念
链表是一种在物理存储结构上非连续学校结构,数据元素的逻辑结构是通过链表中的引用链接次序实现的。
注意:
1.链式结构在逻辑上是连续的,但是在物理上不一定连续。
2.节点(结点)一般都是从堆上申请出来的。
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间有可能连续,但也有可能不连续。
1.2链表的8种结构
根据带头/不带头、单向/双向、循环/非循环来划分,链表有8种结构。
- 带头单向循环链表
- 不带头单向循环链表
- 带头双向循环链表
- 不带头双向循环链表
- 带头单向非循环链表
- 不带头单向非循环链表
- 带头双向非循环链表
- 不带头双向非循环链表
1.3单链表的实现
我们今天实现的是不带头单向非循环链表
1.单链表的接口
ILinkList.java
java
package MLinkList;
public interface ILinkList {
// 1、无头单向非循环链表实现
//头插法
public void addFirst(int data);
//尾插法
public void addLast(int data);
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index, int data);
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key);
//删除第一次出现关键字为key的节点
public void remove(int key);
//删除所有值为key的节点
public void removeAllKey(int key);
//得到单链表的长度
public int size();
//清空链表
public void clear();
//打印单链表
public void display();
}
1.4单链表的功能实现
1.定义链表节点
我们需要先创建一个内部类,作为链表的节点。并创建个头指针、尾指针。
java
/**
* ListNode 类定义了一个链表节点。
* 这个类包含一个整型数据和一个指向下一个节点的指针。
*/
public class ListNode{
// 节点存储的数据
public int data;
// 指向下一个节点的指针
public ListNode next;
/**
* ListNode 类的构造函数,用于创建一个新的链表节点。
*
* @param data 节点存储的数据。
*/
public ListNode(int data)
{
this.data = data; // 初始化节点数据
}
}
public ListNode head;//头指针
public ListNode last;//尾指针
2.链表的头插
进行头插,需要考虑两种情况:
- 头指针为空,让新节点作为头指针
- 头指针不为空,可以进行头插,头插的时候,先让新节点的指针指向头结点,再让头指针移动到新节点位置。
java
public void addFirst(int data) {
ListNode node = new ListNode(data); // 创建新节点,数据为传入的data
// 判断链表是否为空,如果为空,说明当前没有节点,直接将新节点作为头节点
if (head == null) {
head = node;
return;
}
// 链表不为空时,进行头插操作,新节点的next指向当前头节点,然后更新头节点为新节点
node.next = head;
head = node;
}
3.链表的尾插
进行尾插时,同样需要考虑头结点是否为空
javascript
/**
* 使用尾插法向链表中添加元素。
* 该方法会将指定的元素添加到链表的末尾。
*
* @param data 要添加到链表的数据。
*/
public void addLast(int data) {
ListNode node = new ListNode(data); // 创建新节点,数据为data
if (head == null) {
head = last = node; // 如果链表为空,将新节点同时设置为头节点和尾节点
return;
}
last.next = node; // 将当前尾节点的next指向新节点
last = last.next; // 更新尾节点为新添加的节点
}
4.在指定位置插入数据
我们需要考虑3种情况:
- 要插入的位置是否合法
- index位置是在链表两端还是在链表内部,如果index==0,那么则直接调用头插法即可,如果index=size(),那么直接调用尾插法即可。
- 在链表内部,我们就需要找到index的前一个节点,再进行插入。(这里查找index的前一个节点,我们可以定义一个方法进行查找)
java
private void checkIndex(int index)throws IndexNoLegalException {
if (index < 0 || index > size()) {
throw new IndexNoLegalException("index位置不合法"+"index="+index);
}
}
private ListNode findIndex(int index) {
ListNode cur = head;
while (index-- != 1) {
cur = cur.next;
}
return cur;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index, int data) {
//判断index位置是合法
try {
checkIndex(index);
} catch (IndexNotLegalException e) {
e.printStackTrace();
}
if(index==0) {//头插
addFirst(data);
}
if(index==size()) {//尾插
addLast(data);
}
//创建新节点,并查找index位置的前一个节点
ListNode node = new ListNode(data);
ListNode findlist=findIndex(index);
node.next=findlist.next;
findlist.next=node;
}
5.获取单链表长度&清空单链表&单链表的打印
这里主要是单链表的清空,我们也可以直接让head=last=null,但是更好的是把单链表中的节点都清空。
java
/**
* 获取单链表的长度。
* @return 链表中节点的数目。
*/
public int size() {
int count = 0; // 初始化计数器
ListNode cur = head; // 从头节点开始遍历
while (cur != null) {
count++;
cur = cur.next; // 移动到下一个节点
}
return count; // 返回计数结果
}
/**
* 清空单链表,使其变为空链表。
*/
public void clear() {
if (head == null) {
return; // 如果链表为空,则直接返回
}
ListNode cur = head; // 保存头节点的下一个节点,以避免丢失整个链表
while (cur != null) {
ListNode curN=cur.next;
cur=null;
cur=curN;
}
head = last = null; // 最后将头节点和尾节点都设置为null
}
/**
* 打印单链表中的所有节点数据。
*/
public void display() {
if (head == null) {
return; // 如果链表为空,则直接返回
}
ListNode cur = head; // 从头节点开始遍历
while (cur != null) {
System.out.print(cur.data + " "); // 打印当前节点的数据
cur = cur.next; // 移动到下一个节点
}
System.out.println(); // 换行
}
6.查找关键字key是否在单链表中出现
我们直接遍历单链表一遍即可,找到返回true
java
/**
* 检查单链表中是否包含指定关键字。
* @param key 需要在单链表中查找的关键字。
* @return 如果链表中包含关键字,则返回true;否则返回false。
*/
public boolean contains(int key){
// 如果链表头节点为空,直接返回false,表示链表为空,不可能包含关键字
if(head==null){
return false;
}
// 遍历链表,查找是否存在与key相等的节点
while(head!=null){
if(head.data==key){
// 找到相等的节点,返回true
return true;
}
head=head.next; // 移动到下一个节点继续查找
}
// 遍历完整个链表都没有找到相等的节点,返回false
return false;
}
7.移除第一次出现key关键字的节点&移除所有key关键字的节点
移除出现一次关键字的节点:
- 判断链表是否为空
- 我们可以查找当前节点的下一个节点的值是否与key相同,如果相同,那就让当前节点的指针 指向下一个节点的指针
移除所有出现关键字的节点
- 我们需要用到两个指针,一个用来跟踪prev,一个(cur)用来查找与key相同的节点,如果找到,那就让prev.next指向cur.next,删除key节点。
- 当走完循环时,我们还需要判断头结点是否与key相同,如果相同,那就需要让head指向下一个节点。
java
/**
* 删除链表中第一次出现的关键字为key的节点
* @param key 需要删除的节点的值
* 如果链表为空或者没有找到值为key的节点,则链表结构不变
*/
public void remove(int key){
// 链表为空,直接返回
if(head==null){
return;
}
ListNode cur=head;
// 遍历链表,查找第一个值为key的节点
while(cur!=null) {
// 找到后,删除该节点
if(cur.next.data==key){
cur.next=cur.next.next;
return;
}
cur=cur.next;
}
}
/**
* 删除链表中所有值为key的节点
* @param key 需要删除的节点的值
* 该方法不返回任何内容
*/
public void removeAllKey(int key){
// 如果链表为空,则直接返回
if(head==null){
return;
}
ListNode prev=head;// 记录当前节点的前一个节点
ListNode cur=head.next;// 记录当前操作的节点
while(cur!=null){
if(cur.data==key){
prev.next=cur.next; // 如果当前节点的值等于key,则删除该节点
}else{
prev=cur; // 否则,继续向前遍历
}
cur=cur.next;
}
// 如果头节点的值等于key,则更新头节点
if(head.data==key){
head=head.next;
}
}
这些是单链表的一些基本方法,我们测试一下,确实是我们所想的。
2.链表和顺序表的优缺点
链表:
优点:
1.插入和删除操作简单:由于链表的节点通过指针链接,插入和删除操作只需要修改指针的指向,不需要移动其他元素,因此时间复杂度为O(1)。
2.链表可以根据实际情况动态分配内存空间,只需要在插入新节点时分配新的内存,因此避免了固定大小的存储空间的限制。
缺点:
1.随机访问性能差,链表开辟的空间在内存中上随机开辟的,不一定连续,所以在访问的时候需要遍历查找。时间复杂度为O(N).
2.需要额外的存储空间,由于节点与节点之间是通过指针相连的,所以有额外的空间来存储下一个节点的位置。
顺序表:
优点:
1.支持随机访问:由于顺序表在内存中是连续的,可以通过下标直接访问顺序表中的元素。时间复杂度是O(1).
2.存储效率高,不需要开辟额外的空间存储指针。
缺点:
1.在插入和删除数据的时候,需要搬动其他元素,时间复杂度为O(N)。
2.存储空间固定:如果要扩容的话,可能会造成空间浪费。
3.完整代码
接口在上
单链表功能实现:MyLinkltstto.java
java
package MLinkList;
import SlowList.IndexNoLegalException;
public class MyLinklistto implements ILinkList{
// 1、无头单向非循环链表实现
/**
* ListNode 类定义了一个链表节点。
* 这个类包含一个整型数据和一个指向下一个节点的指针。
*/
public class ListNode{
// 节点存储的数据
public int data;
// 指向下一个节点的指针
public ListNode next;
/**
* ListNode 类的构造函数,用于创建一个新的链表节点。
*
* @param data 节点存储的数据。
*/
public ListNode(int data)
{
this.data = data; // 初始化节点数据
}
}
public ListNode head;//头指针
public ListNode last;//尾指针
/**
* 在链表的头部进行插入操作。
* 该方法会将新节点插入到链表的头部,如果链表为空,则新节点成为头节点;
* 如果链表不为空,则新节点将成为新的头节点,原头节点成为新节点的下一个节点。
*
* @param data 要插入节点的数据。
*/
public void addFirst(int data) {
ListNode node = new ListNode(data); // 创建新节点,数据为传入的data
// 判断链表是否为空,如果为空,说明当前没有节点,直接将新节点作为头节点
if (head == null) {
head =last= node;
return;
}
// 链表不为空时,进行头插操作,新节点的next指向当前头节点,然后更新头节点为新节点
node.next = head;
head = node;
}
/**
* 使用尾插法向链表中添加元素。
* 该方法会将指定的元素添加到链表的末尾。
*
* @param data 要添加到链表的数据。
*/
public void addLast(int data) {
ListNode node = new ListNode(data); // 创建新节点,数据为data
if (head == null) {
head = last = node; // 如果链表为空,将新节点同时设置为头节点和尾节点
return;
}
last.next = node; // 将当前尾节点的next指向新节点
last = last.next; // 更新尾节点为新添加的节点
}
/**
* 检查索引是否合法。
* @param index 要检查的索引值。
* @throws IndexNoLegalException 如果索引不合法,则抛出异常。
*/
private void checkIndex(int index)throws IndexNoLegalException {
if (index < 0 || index > size()) { // 索引超出范围不合法
throw new IndexNoLegalException("index位置不合法"+"index="+index);
}
}
/**
* 查找指定索引位置的节点。
* @param index 目标索引位置。
* @return 返回指定索引位置的节点。
*/
private ListNode findIndex(int index) {
ListNode cur = head; // 从头节点开始遍历
while (index-- != 1) { // 递减索引,找到目标位置的前一个节点
cur = cur.next;
}
return cur;
}
/**
* 在任意位置插入一个节点。
* @param index 插入位置的索引。
* @param data 要插入的数据。
*/
public void addIndex(int index, int data) {
// 判断插入位置是否合法
try {
checkIndex(index);
} catch (IndexNotLegalException e) {
e.printStackTrace();
}
if(index==0) { // 如果是插入到头部
addFirst(data);
}
if(index==size()) { // 如果是插入到尾部
addLast(data);
}
// 创建新节点,并插入到指定位置
ListNode node = new ListNode(data);
ListNode findlist=findIndex(index); // 找到插入位置的前一个节点
node.next=findlist.next; // 新节点接在前一个节点之后
findlist.next=node; // 前一个节点指向新节点
}
/**
* 检查单链表中是否包含指定关键字。
* @param key 需要在单链表中查找的关键字。
* @return 如果链表中包含关键字,则返回true;否则返回false。
*/
public boolean contains(int key){
// 如果链表头节点为空,直接返回false,表示链表为空,不可能包含关键字
if(head==null){
return false;
}
// 遍历链表,查找是否存在与key相等的节点
while(head!=null){
if(head.data==key){
// 找到相等的节点,返回true
return true;
}
head=head.next; // 移动到下一个节点继续查找
}
// 遍历完整个链表都没有找到相等的节点,返回false
return false;
}
/**
* 删除链表中第一次出现的关键字为key的节点
* @param key 需要删除的节点的值
* 如果链表为空或者没有找到值为key的节点,则链表结构不变
*/
public void remove(int key){
// 链表为空,直接返回
if(head==null){
return;
}
ListNode cur=head;
// 遍历链表,查找第一个值为key的节点
while(cur!=null) {
// 找到后,删除该节点
if(cur.next.data==key){
cur.next=cur.next.next;
return;
}
cur=cur.next;
}
}
/**
* 删除链表中所有值为key的节点
* @param key 需要删除的节点的值
* 该方法不返回任何内容
*/
public void removeAllKey(int key){
// 如果链表为空,则直接返回
if(head==null){
return;
}
ListNode prev=head;// 记录当前节点的前一个节点
ListNode cur=head.next;// 记录当前操作的节点
while(cur!=null){
if(cur.data==key){
prev.next=cur.next; // 如果当前节点的值等于key,则删除该节点
}else{
prev=cur; // 否则,继续向前遍历
}
cur=cur.next;
}
// 如果头节点的值等于key,则更新头节点
if(head.data==key){
head=head.next;
}
}
/**
* 获取单链表的长度。
* @return 链表中节点的数目。
*/
public int size() {
int count = 0; // 初始化计数器
ListNode cur = head; // 从头节点开始遍历
while (cur != null) {
count++;
cur = cur.next; // 移动到下一个节点
}
return count; // 返回计数结果
}
/**
* 清空单链表,使其变为空链表。
*/
public void clear() {
if (head == null) {
return; // 如果链表为空,则直接返回
}
ListNode cur = head; // 保存头节点的下一个节点,以避免丢失整个链表
while (cur != null) {
ListNode curN=cur.next;
cur=null;
cur=curN;
}
head = last = null; // 最后将头节点和尾节点都设置为null
}
/**
* 打印单链表中的所有节点数据。
*/
public void display() {
if (head == null) {
return; // 如果链表为空,则直接返回
}
ListNode cur = head; // 从头节点开始遍历
while (cur != null) {
System.out.print(cur.data + " "); // 打印当前节点的数据
cur = cur.next; // 移动到下一个节点
}
System.out.println(); // 换行
}
}
测试:test2.java
java
package MLinkList;
public class test2 {
public static void main(String[] args) {
MyLinklistto list = new MyLinklistto();
list.addFirst(2);
list.addFirst(3);
list.addFirst(4);
list.addFirst(4);
list.addFirst(4);
list.addLast(5);
list.addIndex(2, 6);
list.display();
System.out.println("=========");
list.removeAllKey(4);
list.display();
}
}