开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情
前言
之前我们介绍了使用顺序存储结构实现顺序表,我们发现虽然顺序表的查询很快,时间复杂度为 O(1),但是增删的效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。这个问题有没有解决方案呢?
答:有,我们可以使用另外一种存储结构实现线性表:链式存储结构。
一、链表介绍
链表是一种物理存储单元上非连续,非顺序的存储结构,其物理结构不能直观的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的节点(链表中的每一个元素称为节点)组成,节点可以运行时动态生成,如下图演示:
那我们如何使用链表呢?按照面向对象的思想,我们可以设计一个类,来描述节点这个事物,用一个属性描述这个节点存储的元素,用另外一个属性描述这个节点的下一个节点。
节点 API 设计:
| 类名 | Node<T> |
|---|---|
| 构造方法 | Node(T t,Node next) :创建 Node 对象 |
| 成员变量 | T item :存储数据 Node next :指向下一个节点 |
具体实现:
public class Node<T>{
//存储元素
public T item;
//指向下一个节点
public Node next;
public Node(T item,Node next){
this.item = item;
this.next = next;
}
}
//测试:生成链表
public class Test {
public static void main(String[] args) {
//构建节点
Node<Integer> first = new Node<>(11, null);
Node<Integer> second = new Node<>(13, null);
Node<Integer> third = new Node<>(12, null);
Node<Integer> four = new Node<>(8, null);
Node<Integer> five = new Node<>(9, null);
//生成链表
first.next = second;
second.next = third;
third.next = four;
four.next = five;
}
}
链表主要分为两种:
1、单向链表
2、双向链表
二、单向链表
单向链表是链表的一种,它由多个节点组成,每个节点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继节点。链表的头节点的数据域不存储数据,指针域指向第一个真正存储数据的节点。
1.1、单向链表 API 设计
| 类名 | LinkList<T> |
|---|---|
| 构造方法 | LinkList() :创建 LinkList 对象 |
| 成员方法 | 1、public void clean() :清空线性表 2、public boolean isEmpty() :线性表是否为空 3、public int length() :获取线性表元素的个数 4、public T get(int i) :获取线性表中第 i 个位置元素的值 5、public void insert(int i,T t) :在第 i 个元素之前插入一个值为 t 的数据元素 6、public void insert(T t) :向线性表中添加一个元素 t 7、public T remove(int i) :删除并返回线性表中第 i 个位置的数据元素 8、public int indexOf(T t) :返回线性表中首次出现的指定的数据元素的位序号,若不存在,则返回 -1 |
| 成员内部类 | private class Node<T> :节点类 |
| 成员变量 | 1、private Node head :记录头节点 2、private int N :记录链表的长度 |
具体实现:
public class LinkList<T> {
//记录头节点
private Node head;
//记录链表的长度
private int N;
private class Node{
private T item;
private Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
public LinkList() {
//初始化头节点
head = new Node(null, null);
//初始化元素个数
this.N = 0;
}
public void clear(){
//将当前头节点的next断掉,那么就找不到这条链表了
head.next = null;
//将链表的长度设置为0
this.N = 0;
}
public boolean isEmpty(){
return this.N == 0;
}
public int length(){
return N;
}
public T get(int i){
//从第 0 个元素开始找,找 i 次,就是第 i 个节点的数据
Node node = head.next;
if(node == null) return null;
for (int i1 = 0; i1 < i; i1++) {
node = node.next;
}
if(node == null) return null;
return node.item;
}
public void insert(T t){
Node node = head;
while (node.next != null){
node = node.next;
}
//执行到这里,说明到了尾节点
//构建一个新的节点
Node newNode= new Node(t,null);
//将尾巴节点的 next 指向它
node.next = newNode;
//链表长度 +1
N++;
}
public void insert(int index,T t){
//找到 i 的前一个节点和当前 i 节点
Node pre = head;
for (int i = 0; i <= index - 1; i++) {
pre = pre.next;
}
Node current = pre.next;
//创建新的节点,将前一个节点指向插入的节点,当前插入节点指向下一个节点
Node newNode = new Node(t, current);
pre.next = newNode;
//元素加一
N++;
}
public T remove(int index){
//找到当前 i 的前一个节点
Node pre = head;
for (int i = 0; i <= index - 1; i++) {
pre = pre.next;
}
//找到当前节点
Node current = pre.next;
//将前一个节点的 next 指向当前节点的 next
pre.next = current.next;
//元素个数减一
N--;
return current.item;
}
public int indexOf(T t){
int index = 0;
Node node = head.next;
while (node != null){
if(node.item.equals(t)){
return index;
}
node = node.next;
index++;
}
return -1;
}
}
1.2、单向链表遍历
在 Java 中,遍历集合的方式一般都是用 foreach 循环,如果想让我们的 SequenceList 也能支持 foreach ,则需要做如下操作:
1、让 LinkList 实现 Iterable 接口,重写 iterator 接口
2、在 LinkList 内部提供一个内部类 LIterator,实现 Iterator 接口,重写 hasNext() 方法和 next() 方法
具体实现如下:
class LinkList<T> implements Iterable<T>{
//记录头节点
private Node head;
//记录链表的长度
private int N;
private class Node{
private T item;
private Node next;
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
public LinkList() {
//初始化头节点
head = new Node(null, null);
//初始化元素个数
this.N = 0;
}
public void clear(){
//将当前头节点的next断掉,那么就找不到这条链表了
head.next = null;
//将链表的长度设置为0
this.N = 0;
}
public boolean isEmpty(){
return this.N == 0;
}
public int length(){
return N;
}
public T get(int i){
//从第 0 个元素开始找,找 i 次,就是第 i 个节点的数据
Node node = head.next;
if(node == null) return null;
for (int i1 = 0; i1 < i; i1++) {
node = node.next;
}
if(node == null) return null;
return node.item;
}
public void insert(T t){
Node node = head;
while (node.next != null){
node = node.next;
}
//执行到这里,说明到了尾节点
//构建一个新的节点
Node newNode= new Node(t,null);
//将尾巴节点的 next 指向它
node.next = newNode;
//链表长度 +1
N++;
}
public void insert(int index,T t){
//找到 i 的前一个节点和当前 i 节点
Node pre = head;
for (int i = 0; i <= index - 1; i++) {
pre = pre.next;
}
Node current = pre.next;
//创建新的节点,将前一个节点指向插入的节点,当前插入节点指向下一个节点
Node newNode = new Node(t, current);
pre.next = newNode;
//元素加一
N++;
}
public T remove(int index){
//找到当前 i 的前一个节点
Node pre = head;
for (int i = 0; i <= index - 1; i++) {
pre = pre.next;
}
//找到当前节点
Node current = pre.next;
//将前一个节点的 next 指向当前节点的 next
pre.next = current.next;
//元素个数减一
N--;
return current.item;
}
public int indexOf(T t){
int index = 0;
Node node = head.next;
while (node != null){
if(node.item.equals(t)){
return index;
}
node = node.next;
index++;
}
return -1;
}
//================================== 新增部分代码 start =================================
@NonNull
@Override
public Iterator<T> iterator() {
return new LIterator();
}
private class LIterator implements Iterator<T>{
private Node curr;
public LIterator() {
curr = head;
}
@Override
public boolean hasNext() {
return curr.next != null;
}
@Override
public T next() {
curr = curr.next;
return curr.item;
}
}
//================================== 新增部分代码 end =================================
}
1.3、单向链表测试
public class Test {
public static void main(String[] args) {
LinkList<String> strList = new LinkList<>();
//1、插入元素
strList.insert("姚明");
strList.insert("科比");
strList.insert("麦迪");
strList.insert(1,"詹姆斯");
//2、遍历
for (String s : strList) {
System.out.println(s);
//姚明
//詹姆斯
//科比
//麦迪
}
//3、获取元素
String result = strList.get(1);
System.out.println(result);//詹姆斯
//4、删除元素
String removeElement = strList.remove(0);
System.out.println(removeElement);//姚明
//5、测试清空
strList.clear();
System.out.println(strList.length());//0
}
}
三、单向链表时间复杂度分析
3.1、get(i) 方法时间复杂度
get(i) 每一次查询,都要从链表的头开始,依次向后查询,随着数据元素 N 的增加,比较的元素也越多,时间复杂度为 O(n)
3.2、insert(int i,T t) 方法时间复杂度
insert(int i,T t) 每一次插入,需要先找到 i 位置的前一个元素,然后完成插入操作,随着数据元素 N 的增多,查找的元素越多,时间复杂度为 O(n)
3.3、remove(int i) 方法时间复杂度
remove(int i) 每一次删除,需要先找到 i 位置的前一个元素,然后完成移除操作,随着数据元素 N 的增多,查找的元素越多,时间复杂度为 O(n)
相比较顺序表:
1、链表插入和删除时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预先指定存储空间大小,或者存储过程中涉及扩容等操作,同时它并没有涉及元素的交换。
2、链表的查询操作性能会比较低,因此如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。
四、总结
本篇文章我们介绍了:
1、链表的节点及具体实现
2、单向链表的 API 设计,到具体实现,到添加增强 for 循环遍历,到用例测试
3、单向链表的时间复杂度分析:
1、get(i) :O(n)
2、insert(int i,T t) :O(n)
3、remove(int i) :O(n)
虽然链表的插入和删除的时间复杂度和顺序表一样,但是它不需要预先指定存储空间以及扩容等操作,效率还是很高的,因此涉及到增删比较多的情况使用链表,查询比较多的则使用顺序表
好了,本篇文章到这里就结束了,感谢你的阅读🤝