1 定义
线性表是零个或多个数据元素的有限序列。
2 抽象数据类型
ADT 线性表(List)
Data:线性表的数据对象集合为{al,a2,a3,....an},每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个后继元素。数据元素之间的关系是一对一的关系。
Operation:
init(int maxSize):初始化操作,建立一个空的线性表;
isEmpty():若线性表为空,返回true,否则返回false;
clear():清空线性表;
getElement(int position):获取线性表中第position个元素值;
locateElement(DataType e):在线性表中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号,否则返回0表示失败;
insert(DataType e, int position):在线性表的第position个位置插入元素e;
delete(int position):删除纯属表中第position个位置的元素;
length():返回线性表的元素个数;
endADT
3 顺序存储结构
3.1 概述
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
线性表的顺序存储结构的抽象数据类型在标准类型的基础上,新增了length和maxSize两个属性:
ADT 线性表(List)
Data:线性表的数据对象集合为{al,a2,a3,....an},每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个后继元素。数据元素之间的关系是一对一的关系。
length:线性表中实际元素个数,应小于等于maxSize。
maxSize:线性表可以存储的元素个数。
Operation:
init(int maxSize):初始化操作,建立一个空的线性表;
isEmpty():若线性表为空,返回true,否则返回false;
clear():清空线性表;
getElement(int position):获取线性表中第position个元素值;
locateElement(DataType e):在线性表中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号,否则返回0表示失败;
insert(DataType e, int position):在线性表的第position个位置插入元素e;
delete(int position):删除纯属表中第position个位置的元素;
length():返回线性表的元素个数;
endADT
3.2 初始化
定义一个空的线性表,初始化maxSize和data数组:
/**
* 初始化线性表
*
* @param maxSize 线性表最大长度
* @author Korbin
* @date 2023-01-04 18:29:17
**/
public void init(int maxSize) {
this.maxSize = maxSize;
data = new Object[maxSize];
}
3.3 判断是否为空
判断length是否为0即可,为避免其他异常,使用小于等于,如下:
/**
* 判断线性表是否为空
*
* @return 线性表为空返回true,非空返回false
* @author Korbin
* @date 2023-01-11 15:54:53
**/
public boolean isEmpty() {
return length <= 0;
}
3.4 清空线性表
把length和data重新初始化即可(C语言则需要回收内存):
/**
* 清空线性表
*
* @author Korbin
* @date 2023-01-11 15:58:15
**/
public void clear() {
this.length = 0;
this.data = new Object[this.maxSize];
}
3.5 获取指定位置元素
线性表长度为0或要获取的位置小于1或大于线性表长度时抛出异常,实际位置是索引加1:
/**
* 获取线性表中的第position个元素
*
* @param position 位置
* @return 获取到的元素
* @throws RuntimeException 线性表长度为0或要获取的位置小于1或大于线性表长度时抛出异常
* @author Korbin
* @date 2023-01-04 18:32:10
**/
@SuppressWarnings("unchecked")
public T getElement(int position) {
if (position < 1 || position > length) {
throw new RuntimeException("error position");
} else {
return (T) (data[position - 1]);
}
}
时间复杂度为O(1)。
3.6 在线性表中查找与给定值e相等的元素
使用equals判断是否相等,因此DataType需要实现equals方法:
/**
* 查找element在线性表中的位置
*
* @param element 待查找的元素
* @return 元素在线性表中的位置
* @author Korbin
* @date 2023-01-11 15:59:23
**/
@SuppressWarnings("unchecked")
public int locateElement(T element) {
int position = -1;
for (int i = 0; i < length; i++) {
T e = (T) (data[i]);
if (e.equals(element)) {
position = i;
break;
}
}
return position + 1;
}
时间复杂度为O(n)。
3.7 插入操作
线性表已满,或位置异常时抛出异常,插入时,其后的所有元素需要往后移,实际元素个数要加1:
/**
* 在线性表中的指定位置position插入一个元素
*
* @param position 指定位置
* @param element 要插入的元素
* @throws RuntimeException 线性表已满,或位置异常时抛出异常
* @author Korbin
* @date 2023-01-04 19:22:34
**/
public void insert(int position, T element) {
if (length == maxSize) {
throw new RuntimeException("already full");
}
if (position < 1 || position > length + 1) {
throw new RuntimeException("error position");
}
if (position <= length) {
for (int k = length - 1; k >= position - 1; k--) {
data[k + 1] = data[k];
}
}
data[position - 1] = element;
length++;
}
时间复杂度为O(n)。
3.8 删除操作
线性表为空或位置不对时抛出异常,实际元素个数要减1:
/**
* 删除线性表中第position位的元素
*
* @param position 位置
* @throws RuntimeException 线性表为空或位置不对时抛出异常
* @author Korbin
* @date 2023-01-05 09:46:58
**/
public void delete(int position) {
if (length == 0) {
throw new RuntimeException("empty list");
}
if (position < 1 || position > length) {
throw new RuntimeException("error position");
}
if (position < length) {
for (int k = position; k < length; k++) {
data[k - 1] = data[k];
}
}
// 如果要删除的位置就是线性表的长度所在的位置的话,那直接把长度减1就行了
length--;
}
时间复杂度为O(n)。
3.9 返回实际元素个数
/**
* 获取线性表真实长度
*
* @return 线性表长度
* @author Korbin
* @date 2023-01-05 10:15:41
**/
public int length() {
return length;
}
3.10 完整代码
/**
* 顺序存储线性表
*
* @author Korbin
* @date 2023-01-04 18:19:38
**/
public class SequentialList<T> {
/**
* 线性表数据
**/
private Object[] data;
/**
* 线性表当前长度
**/
private int length;
/**
* 线性表最大长度
*/
private int maxSize = 20;
/**
* 清空线性表
*
* @author Korbin
* @date 2023-01-11 15:58:15
**/
public void clear() {
this.length = 0;
this.data = new Object[this.maxSize];
}
/**
* 删除线性表中第position位的元素
*
* @param position 位置
* @throws RuntimeException 线性表为空或位置不对时抛出异常
* @author Korbin
* @date 2023-01-05 09:46:58
**/
public void delete(int position) {
if (length == 0) {
throw new RuntimeException("empty list");
}
if (position < 1 || position > length) {
throw new RuntimeException("error position");
}
if (position < length) {
for (int k = position; k < length; k++) {
data[k - 1] = data[k];
}
}
// 如果要删除的位置就是线性表的长度所在的位置的话,那直接把长度减1就行了
length--;
}
/**
* 获取线性表中的第position个元素
*
* @param position 位置
* @return 获取到的元素
* @throws RuntimeException 线性表长度为0或要获取的位置小于1或大于线性表长度时抛出异常
* @author Korbin
* @date 2023-01-04 18:32:10
**/
@SuppressWarnings("unchecked")
public T getElement(int position) {
if (position < 1 || position > length) {
throw new RuntimeException("error position");
} else {
return (T) (data[position - 1]);
}
}
/**
* 初始化线性表
*
* @param maxSize 线性表最大长度
* @author Korbin
* @date 2023-01-04 18:29:17
**/
public void init(int maxSize) {
this.maxSize = maxSize;
data = new Object[maxSize];
}
/**
* 在线性表中的指定位置position插入一个元素
*
* @param position 指定位置
* @param element 要插入的元素
* @throws RuntimeException 线性表已满,或位置异常时抛出异常
* @author Korbin
* @date 2023-01-04 19:22:34
**/
public void insert(int position, T element) {
if (length == maxSize) {
throw new RuntimeException("already full");
}
if (position < 1 || position > length + 1) {
throw new RuntimeException("error position");
}
if (position <= length) {
for (int k = length - 1; k >= position - 1; k--) {
data[k + 1] = data[k];
}
}
data[position - 1] = element;
length++;
}
/**
* 判断线性表是否为空
*
* @return 线性表为空返回true,非空返回false
* @author Korbin
* @date 2023-01-11 15:54:53
**/
public boolean isEmpty() {
return length <= 0;
}
/**
* 获取线性表真实长度
*
* @return 线性表长度
* @author Korbin
* @date 2023-01-05 10:15:41
**/
public int length() {
return length;
}
/**
* 查找element在线性表中的位置
*
* @param element 待查找的元素
* @return 元素在线性表中的位置
* @author Korbin
* @date 2023-01-11 15:59:23
**/
@SuppressWarnings("unchecked")
public int locateElement(T element) {
int position = -1;
for (int i = 0; i < length; i++) {
T e = (T) (data[i]);
if (e.equals(element)) {
position = i;
break;
}
}
return position + 1;
}
/**
* 打印线性表
*
* @throws RuntimeException 线性表为空时抛出异常
* @author Korbin
* @date 2023-01-05 09:55:55
**/
@Override
public String toString() {
if (length == 0) {
throw new RuntimeException("empty list");
}
StringBuilder result = new StringBuilder("[");
for (int i = 0; i < length; i++) {
result.append(i + 1).append(":").append(data[i]).append(",");
}
result.append("]");
return result.toString();
}
}
3.11 优缺点
4 链式存储结构
4.1 概述
用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的,只是通过数据元素中的后继元素地址来指向后继元素的存储地址,即称为链式存储。
在链式存储结构中,每个数据元素除了存储其本身的信息外,还需存储一个指示其直接后继的信息,即直接后继的存储位置,存储数据元素信息的域称为数据域,存储直接后继位置的域称为指针域,指针域中存储的信息称做指针或链。数据域和指针域组成数据元素的存储映像,称为结点。
因为每个结点中只包含一个指针域,因此这种链表也被称为单链表。
单链表的第一个结点称为头结点,头结点不存储数据,只存储链表中第一个实际元素的存储位置,称为头指针。
单链表的最后一个结点没有后继元素,因此其指针域的值是Null。
单链表的抽象存储结构与线性表的顺序存储结构的抽象存储结构略有区别:
ADT 线性表(List)
Data:线性表的数据对象集合为{al,a2,a3,....an},每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个后继元素。数据元素之间的关系是一对一的关系。单链表中只存储头结点对象即可。
length:线性表中实际元素个数。
Operation:
init:初始化操作,建立一个空的线性表;
isEmpty:若线性表为空,返回true,否则返回false;
clear:清空线性表;
getElement(int position):获取线性表中第position个元素值;
locateElement(DataType e):在线性表中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号,否则返回0表示失败;
insert(DataType e, int position):在线性表的第position个位置插入元素e;
delete(int position):删除纯属表中第position个位置的元素;
length():返回线性表的元素个数;
endADT
4.2 结点结构
DataType需要进行特殊定义,必须包含后继元素指针,如下:
import lombok.Data;
import java.util.UUID;
/**
* 单链表
*
* @author Korbin
* @date 2023-01-05 10:58:05
**/
@Data
public class LinkListNode<T> {
/**
* 数据
**/
private T data;
/**
* 设置一个ID,已进行equals判断
**/
private String id = UUID.randomUUID().toString();
/**
* 后继结点
**/
private LinkListNode<T> next;
/**
* 构造子
*
* @author Korbin
* @date 2023-01-05 12:38:08
**/
public LinkListNode() {
}
@Override
@SuppressWarnings("unchecked")
public boolean equals(Object obj) {
if (obj instanceof LinkListNode) {
try {
LinkListNode<T> node = (LinkListNode<T>) obj;
return data.equals(node.getData()) && id.equals(node.getId());
} catch (ClassCastException e) {
return false;
}
} else {
return false;
}
}
@Override
public String toString() {
return "{id=" + id + ", data=" + data.toString() + "}";
}
}
构造子、equals和toString方法可以按需,添加一个id的主要目的是用于equals,如有其他判断相等的方式,也可以不加id。
4.3 初始化和清空
初始化和清除在Java中因为垃圾回收机制,因此实现方式相同。
单链表定义的Data,不需要是一个数组,只需要放置headNode,即头结点即可,因此初始化和清空时只需要将length置为0,将headNode的值和后继结点置为空即可。
/**
* 清空
* <p>
* 直接调用初始化方法即可
*
* @author Korbin
* @date 2023-01-11 15:58:15
**/
public void clear() {
init();
}
/**
* 初始化
*
* @author Korbin
* @date 2023-01-04 18:29:17
**/
public void init() {
headNode = new LinkListNode<>();
headNode.setData(null);
headNode.setNext(null);
length = 0;
}
4.4 获取指定位置的元素
从头结点开始,迭代后继结点:
/**
* 获取单链表中第position位的元素的值
*
* @param position 要获取的结点所在的位置
* @return 指定位置结点的值
* @throws RuntimeException 未获取到结点时抛出
* @author Korbin
* @date 2023-01-05 11:06:01
**/
public LinkListNode<T> getElement(int position) {
if (position == 0) {
return headNode;
}
LinkListNode<T> result = headNode.getNext();
int j = 1;
while (null != result && j < position) {
result = result.getNext();
j++;
}
if (null == result || j > position) {
throw new RuntimeException("node not exists");
}
return result;
}
时间复杂度为O(n)。
4.5 插入操作
先获取待插入位置的前驱结点,然后让前驱结点指向新结点,新结点指向原结点即可:
/**
* 在单链表的指定位置插入一个结点
*
* @param position 要插入的位置
* @param value 要插入的结点的值
* @throws RuntimeException 要插入的位置不存在结点时抛出
* @author Korbin
* @date 2023-01-05 11:12:05
**/
public void insert(int position, T value) {
LinkListNode<T> positionNode = getElement(position - 1);
LinkListNode<T> newNode = new LinkListNode<>();
newNode.setData(value);
newNode.setNext(positionNode.getNext());
positionNode.setNext(newNode);
length++;
}
时间复杂度为O(n)。
4.6 删除操作
先获取待删除结点的前驱结点,然后让前驱结点指向其后继的后继即可:
/**
* 删除单链表中指定位置的元素
*
* @param position 要删除结点所在位置
* @throws RuntimeException 要删除的位置不存在结点时抛出异常
* @author Korbin
* @date 2023-01-05 11:15:21
**/
public void delete(int position) {
LinkListNode<T> lastNode = getElement(position - 1);
lastNode.setNext(lastNode.getNext().getNext());
length--;
}
删除操作的时间复杂度为O(n)。
4.7 查找与给定值e相等的元素
从头结点开始迭代后继结点,直到没有后续结点或者找到的结点等于给定的元素:
/**
* 查找element在单链表中的位置
*
* @param element 待查找的元素
* @return 元素在单链表中的位置
* @author Korbin
* @date 2023-01-11 15:59:23
**/
public int locateElement(LinkListNode<T> element) {
LinkListNode<T> result = headNode.getNext();
int j = 1;
while (null != result) {
if (result.equals(element)) {
break;
}
result = result.getNext();
j++;
}
if (null == result) {
throw new RuntimeException("node not exists");
}
return j;
}
4.8 完整代码
/**
* 单链表
*
* @author Korbin
* @date 2023-01-05 16:19:13
**/
public class LinkList<T> {
/**
* 头结点
**/
private LinkListNode<T> headNode;
/**
* 结点数量
**/
private int length;
/**
* 清空
* <p>
* 直接调用初始化方法即可
*
* @author Korbin
* @date 2023-01-11 15:58:15
**/
public void clear() {
init();
}
/**
* 删除单链表中指定位置的元素
*
* @param position 要删除结点所在位置
* @throws RuntimeException 要删除的位置不存在结点时抛出异常
* @author Korbin
* @date 2023-01-05 11:15:21
**/
public void delete(int position) {
LinkListNode<T> lastNode = getElement(position - 1);
lastNode.setNext(lastNode.getNext().getNext());
length--;
}
/**
* 获取单链表中第position位的元素的值
*
* @param position 要获取的结点所在的位置
* @return 指定位置结点的值
* @throws RuntimeException 未获取到结点时抛出
* @author Korbin
* @date 2023-01-05 11:06:01
**/
public LinkListNode<T> getElement(int position) {
if (position == 0) {
return headNode;
}
LinkListNode<T> result = headNode.getNext();
int j = 1;
while (null != result && j < position) {
result = result.getNext();
j++;
}
if (null == result || j > position) {
throw new RuntimeException("node not exists");
}
return result;
}
/**
* 初始化
*
* @author Korbin
* @date 2023-01-04 18:29:17
**/
public void init() {
headNode = new LinkListNode<>();
headNode.setData(null);
headNode.setNext(null);
length = 0;
}
/**
* 在单链表的指定位置插入一个结点
*
* @param position 要插入的位置
* @param value 要插入的结点的值
* @throws RuntimeException 要插入的位置不存在结点时抛出
* @author Korbin
* @date 2023-01-05 11:12:05
**/
public void insert(int position, T value) {
LinkListNode<T> positionNode = getElement(position - 1);
LinkListNode<T> newNode = new LinkListNode<>();
newNode.setData(value);
newNode.setNext(positionNode.getNext());
positionNode.setNext(newNode);
length++;
}
/**
* 判断是否为空
*
* @return 为空返回true,非空返回false
* @author Korbin
* @date 2023-01-11 15:54:53
**/
public boolean isEmpty() {
return null == headNode.getNext() && length == 0;
}
/**
* 获取真实长度
*
* @return 长度
* @author Korbin
* @date 2023-01-05 10:15:41
**/
public int length() {
return length;
}
/**
* 查找element在单链表中的位置
*
* @param element 待查找的元素
* @return 元素在单链表中的位置
* @author Korbin
* @date 2023-01-11 15:59:23
**/
public int locateElement(LinkListNode<T> element) {
LinkListNode<T> result = headNode.getNext();
int j = 1;
while (null != result) {
if (result.equals(element)) {
break;
}
result = result.getNext();
j++;
}
if (null == result) {
throw new RuntimeException("node not exists");
}
return j;
}
@Override
public String toString() {
LinkListNode<T> next = headNode.getNext();
StringBuilder builder = new StringBuilder("[");
while (null != next) {
builder.append(next).append(",");
next = next.getNext();
}
builder.append("length=").append(length);
builder.append("]");
return builder.toString();
}
}
4.9 单链表结构与顺序存储结构优缺点
因此,通常情况下:
(1) 需要频繁查找,很少进行插入和删除时,建议使用顺序存储结构;
(2) 需要频繁插入和删除时,建议使用单链表结构,实际上,如果需要批量插入或删除时,单链表的优势才能体现出来,即,如果需要插入/删除指定位置的10个元素时,单链表只需要迭代一次找到待插入/删除位置的元素,然后就可以直接对10个元素进行操作了,而顺序存储结构则需要将待插入/删除元素之后的元素移动;
(3) 元素个数变化较大或根本不知道多大时,建议使用单链表,否则使用顺序存储结构;
5 静态链表
5.1 概述
在一些没有指针也没有引用的语言中,用数组来代替指针,来描述的单链表,称为静态链表。
在静态链表中,每一个结点由两个数据域组成,data用于存储数据,cursor用于存储后继结点的指针(数组的索引),使用cursor代替单链表中的后继结点。
由于静态链表中存储的数据是一个数组,因此静态链表也会定义length和maxSize,分别用于表示结点的实际数量和最多可存放的结点数量。
数组的所有元素并不都存储着数据(结点),未存储数据的元素被称为备用链表,而数组的第一个元素,即下标为0的元素存储的结点不包含数据,只包含cursor,cursor存放着备用链表的第一个结点的下标,即下一个可存储数据的数组下标。
数组的最后一个元素存储的结点也不存储数据,只包含cursor,cursor存放着第一个结点的下标,即头结点的下标,因此数组的最后一个元素起着头指针的作用。
因此,静态链表初始化状态会如下所示:
下标当然是从0、1、2一直到999,而cursor比较特殊:
(1) 假设数组为array,那么array[0]的cursor值指向下一个可存储结点的元素的下标,因此应为1;
(2) array[1]还没有存储结点,那我们让它的cursor指向下一个结点,即array[2],因此array[1]的cursor为2,依此类推;
(3) 最后一个节点,array[999],它应该指向第一个结点的下标,此时还没有任何一个结点,因此array[999]的cursor应为0;
此时,假设静态链表中依次存储了甲、乙、丁、戊、己、庚,则其结构应如下所示:
(1) array[0],不存储数据,存储的是下一个可存储数据的下标,如图,下一个可存储的下标是7,因此array[0]的cursor为7;
(2) array[1],存储第1个结点甲,指向第二个结点乙,乙是array[2],因此array[1]的cursor为array[2]的下标,即2,依此类推;
(3) array[6],此时静态链表中的最后一个结点,存储的数据是庚,因其没有后继结点,因此令其cursor为0;
(4) array[999],作为数组的最后一个元素,其存储的是静态链表中第一个结点的下标,我们知道,静态链表中的第一个结点是甲,存储在array[1],下标是1,因此array[999]的cursor是1;
5.2 结点定义
与单链表相同,添加一个属性id用于判断结点是否相等:
import lombok.Data;
import java.util.UUID;
/**
* 静态链表节点
*
* @author Korbin
* @date 2023-01-05 16:25:49
**/
@Data
public class StaticLinkNode<T> {
/**
* 游标,指向下一个节点,为0时表示无指向
**/
private int cursor;
/**
* 节点值
**/
private T data;
/**
* 设置一个ID,已进行equals判断
**/
private String id = UUID.randomUUID().toString();
@Override
@SuppressWarnings("unchecked")
public boolean equals(Object obj) {
if (obj instanceof StaticLinkNode) {
try {
StaticLinkNode<T> node = (StaticLinkNode<T>) obj;
return data.equals(node.getData()) && id.equals(node.getId());
} catch (ClassCastException e) {
return false;
}
} else {
return false;
}
}
}
5.3 初始化和清除
上文有提到初始化过程,即初始化maxSize、data,并设置数组中每个元素的cursor默认值,而清除静态链表和初始化的逻辑是一致的:
/**
* 判断是否为空
*
* @return 为空返回true,非空返回false
* @author Korbin
* @date 2023-01-11 15:54:53
**/
public boolean isEmpty() {
return length == 0;
}
/**
* 初始化静态链表
*
* @param maxSize 链表的最大长度
* @throws RuntimeException 最大长度为0时抛出
* @author Korbin
* @date 2023-01-05 16:30:25
**/
@SuppressWarnings("unchecked")
public void init(int maxSize) {
if (maxSize < 1) {
throw new RuntimeException("error maxSize");
}
this.maxSize = maxSize;
data = new StaticLinkNode[maxSize];
for (int i = 0; i < maxSize - 1; i++) {
StaticLinkNode<T> node = new StaticLinkNode<>();
node.setCursor(i + 1);
data[i] = node;
}
StaticLinkNode<T> node = new StaticLinkNode<>();
node.setCursor(0);
data[maxSize - 1] = node;
length = 0;
}
5.4 插入
插入过程:
(1) 先从array[0]中获取到下一个可存放数据的数组元素下标,设为nextIndex;
(2) 然后找到待插入位置的上一个结点:先从array[length-1]中获取第1个节点下标,然后批第2个节点下标,然后找第3个节点下标,直到找到待插入位置position - 1个节点的下标,设找到的值为lastIndex;
(3) 我们知道,array[0]存储中下一个可存放数据的数组元素下标,也知道array[nextIndex]存储着下下个可存放数据的数组元素下标y,因此如果array[nextIndex]被使用了,那么array[0]应存放的cursor值则为y,因此代码中需要对array[0]的cursor值进行修改;
(4) 然后设置array[nextIndex]的数据域为待插入的结点,cursor则为待插入位置上一个结点的cursor,再把待插入位置上一个结点的cursor设置为nextIndex,即插入的结点下标,那么插入操作就完成了;
(5) 最后注意,结点数量需要加1;
/**
* 下一个可插入的索引位
*
* @return 下一个可插入的索引位
* @author Korbin
* @date 2023-01-05 20:20:14
**/
public int getNextIndex() {
return data[0].getCursor();
}
/**
* 在指定位置插入
*
* @param position 位置
* @param value 要插入的元素
* @throws RuntimeException 插入到数组的0位,或者跳跃进行插入,或者数组长度满后插入时抛出异常
* @author Korbin
* @date 2023-01-05 19:32:06
**/
public void insert(int position, T value) {
// 数组的第0个元素存储着数组下一个可存放节点的位置,获取到这个位置
int nextIndex = getNextIndex();
if (position < 1 || position > length + 1) {
// 插入位置不能小于1,也不能大于链表的实际长度+1
// 可以为length+1的原因,是因为允许在最后面插入
throw new RuntimeException("error position");
}
if (length == maxSize - 2) {
// 如果实际长度等于最大长度时,表示链表已满,不能再插入
throw new RuntimeException("fulled");
}
// 默认数组的最后一个元素存储的是第一个节点的下标
int lastIndex = maxSize - 1;
// 获取到待插入的节点的上一个节点的游标
// 首先通过最后一个节点的游标,可以找到第一个节点
// 通过第一个节点的游标,可以找到第二个节点
// 依此类推
for (int i = 1; i <= position - 1; i++) {
lastIndex = data[lastIndex].getCursor();
}
// 设置第0个元素的游标值为待插入节点的游标值,初始化时有设置
data[0].setCursor(data[nextIndex].getCursor());
// 设置这个节点的节点值为待插入值
data[nextIndex].setData(value);
// 设置这个节点的游标为上一个节点的游标值
data[nextIndex].setCursor(data[lastIndex].getCursor());
// 设置上一个节点的游标值为这个节点的位置
data[lastIndex].setCursor(nextIndex);
++length;
}
时间复杂度为O(position-1),即O(n)。
5.5 删除
(1) 先找到待删除结点的上一个结点,寻找方法与插入时一致,此时找到了lastNode;
(2) 然后找到待删除结点,即lastNode.getCursor()对应的结点,此时找到了toDeleteNode;
(3) 令lastNode指向toDeleteNode的下一个结点,删除操作就完成了;
(4) toDeleteNode被删除了,那么它所在的数组元素,就可以存放新的结点了,这时,令toDeleteNode的cursor指向array[0]的cursor,令array[0]的cursor指向toDeleteNode的下标,即:让toDeleteNode所在的位置成为下一个可存储数据的下标,即备用链表的第一位,同时,把备用链表的其他可存储结点的下标依次往后挪一位;
(5) 最后,注意结点数量应减1;
/**
* 删除指定位置的节点
*
* @param position 待删除节点位置
* @throws RuntimeException 删除第0个节点或者要删除的位置超出节点实际数量时抛出异常
* @author Korbin
* @date 2023-01-05 19:34:18
**/
public void delete(int position) {
// 不得删除第0个元素
// 不得删除超出实际节点数量的元素
if (position == 0 || position > length()) {
throw new RuntimeException("error position");
}
// 找到要删除的节点的上一个节点
int lastIndex = data[maxSize - 1].getCursor();
int lastIndex = maxSize - 1;
// 获取到待插入的节点的上一个节点的游标
// 首先通过最后一个节点的游标,可以找到第一个节点
// 通过第一个节点的游标,可以找到第二个节点
// 依此类推
for (int i = 1; i <= position - 1; i++) {
lastIndex = data[lastIndex].getCursor();
}
StaticLinkNode<T> lastNode = data[lastIndex];
// 待删除的节点
int toDeleteIndex = lastNode.getCursor();
StaticLinkNode<T> toDeleteNode = data[toDeleteIndex];
// 上一个节点的游标指向下一个节点
lastNode.setCursor(toDeleteNode.getCursor());
data[lastIndex] = lastNode;
// 当前节点的值设置为空
toDeleteNode.setData(null);
// 当前节点的游标设置为0
toDeleteNode.setCursor(0);
data[toDeleteIndex] = toDeleteNode;
// 把已删除的节点的索引位置作为下一次插入时的位置
data[toDeleteIndex].setCursor(data[0].getCursor());
data[0].setCursor(toDeleteIndex);
length--;
}
同样,时间复杂度为O(n)。
5.6 获取指定位置的元素
从第一个节点一直往后找即可:
/**
* 获取指定位置的元素
*
* @param position 指定的位置
* @return 指定位置的元素
* @throws RuntimeException 当试图获取第0个或超出实际节点数量的元素时抛出异常
* @author Korbin
* @date 2023-01-05 19:59:26
**/
public StaticLinkNode<T> getElement(int position) {
// 不得获取第0个元素
// 不得获取超出实际节点数量的元素
if (position == 0 || position > length()) {
throw new RuntimeException("error position");
}
// 依次往下迭代即可
int positionIndex = data[maxSize - 1].getCursor();
for (int i = 1; i < position; i++) {
positionIndex = data[positionIndex].getCursor();
}
return data[positionIndex];
}
5.7 获取指定结点在静态链表中的位置
同样,从第一个结点一直往后找:
/**
* 查找element在单链表中的位置
*
* @param element 待查找的元素
* @return 元素在单链表中的位置
* @author Korbin
* @date 2023-01-11 15:59:23
**/
public int locateElement(StaticLinkNode<T> element) {
// 依次往下迭代即可
int index = data[maxSize - 1].getCursor();
for (int i = 0; i < length; i++) {
StaticLinkNode<T> node = data[index];
if (node.equals(element)) {
break;
}
index = data[index].getCursor();
}
return index;
}
5.8 完整代码
/**
* 静态链表
*
* @author Korbin
* @date 2023-01-05 16:27:11
**/
public class StaticLinkList<T> {
/**
* 节点数据
*/
private StaticLinkNode<T>[] data;
/**
* 实际节点数量
**/
private int length;
/**
* 链表的最大长度
*/
private int maxSize;
/**
* 清空
*
* @author Korbin
* @date 2023-01-11 15:58:15
**/
public void clear() {
init(maxSize);
}
/**
* 删除指定位置的节点
*
* @param position 待删除节点位置
* @throws RuntimeException 删除第0个节点或者要删除的位置超出节点实际数量时抛出异常
* @author Korbin
* @date 2023-01-05 19:34:18
**/
public void delete(int position) {
// 不得删除第0个元素
// 不得删除超出实际节点数量的元素
if (position == 0 || position > length()) {
throw new RuntimeException("error position");
}
// 找到要删除的节点的上一个节点
int lastIndex = data[maxSize - 1].getCursor();
int lastIndex = maxSize - 1;
// 获取到待插入的节点的上一个节点的游标
// 首先通过最后一个节点的游标,可以找到第一个节点
// 通过第一个节点的游标,可以找到第二个节点
// 依此类推
for (int i = 1; i <= position - 1; i++) {
lastIndex = data[lastIndex].getCursor();
}
StaticLinkNode<T> lastNode = data[lastIndex];
// 待删除的节点
int toDeleteIndex = lastNode.getCursor();
StaticLinkNode<T> toDeleteNode = data[toDeleteIndex];
// 上一个节点的游标指向下一个节点
lastNode.setCursor(toDeleteNode.getCursor());
data[lastIndex] = lastNode;
// 当前节点的值设置为空
toDeleteNode.setData(null);
// 当前节点的游标设置为0
toDeleteNode.setCursor(0);
data[toDeleteIndex] = toDeleteNode;
// 把已删除的节点的索引位置作为下一次插入时的位置
data[toDeleteIndex].setCursor(data[0].getCursor());
data[0].setCursor(toDeleteIndex);
length--;
}
/**
* 返回节点信息
**/
public StaticLinkNode<T>[] getData() {
return data;
}
/**
* 获取指定位置的元素
*
* @param position 指定的位置
* @return 指定位置的元素
* @throws RuntimeException 当试图获取第0个或超出实际节点数量的元素时抛出异常
* @author Korbin
* @date 2023-01-05 19:59:26
**/
public StaticLinkNode<T> getElement(int position) {
// 不得获取第0个元素
// 不得获取超出实际节点数量的元素
if (position == 0 || position > length()) {
throw new RuntimeException("error position");
}
// 依次往下迭代即可
int positionIndex = data[maxSize - 1].getCursor();
for (int i = 1; i < position; i++) {
positionIndex = data[positionIndex].getCursor();
}
return data[positionIndex];
}
/**
* 获取首节点索引
*
* @return 首节点索引
* @author Korbin
* @date 2023-01-05 20:09:43
**/
public int getFirstNodeIndex() {
return data[maxSize - 1].getCursor();
}
/**
* 下一个可插入的索引位
*
* @return 下一个可插入的索引位
* @author Korbin
* @date 2023-01-05 20:20:14
**/
public int getNextIndex() {
return data[0].getCursor();
}
/**
* 初始化静态链表
*
* @param maxSize 链表的最大长度
* @throws RuntimeException 最大长度为0时抛出
* @author Korbin
* @date 2023-01-05 16:30:25
**/
@SuppressWarnings("unchecked")
public void init(int maxSize) {
if (maxSize < 1) {
throw new RuntimeException("error maxSize");
}
this.maxSize = maxSize;
data = new StaticLinkNode[maxSize];
for (int i = 0; i < maxSize - 1; i++) {
StaticLinkNode<T> node = new StaticLinkNode<>();
node.setCursor(i + 1);
data[i] = node;
}
StaticLinkNode<T> node = new StaticLinkNode<>();
node.setCursor(0);
data[maxSize - 1] = node;
length = 0;
}
/**
* 在指定位置插入
*
* @param position 位置
* @param value 要插入的元素
* @throws RuntimeException 插入到数组的0位,或者跳跃进行插入,或者数组长度满后插入时抛出异常
* @author Korbin
* @date 2023-01-05 19:32:06
**/
public void insert(int position, T value) {
// 数组的第0个元素存储着数组下一个可存放节点的位置,获取到这个位置
int nextIndex = getNextIndex();
if (position < 1 || position > length + 1) {
// 插入位置不能小于1,也不能大于链表的实际长度+1
// 可以为length+1的原因,是因为允许在最后面插入
throw new RuntimeException("error position");
}
if (length == maxSize - 2) {
// 如果实际长度等于最大长度时,表示链表已满,不能再插入
throw new RuntimeException("fulled");
}
// 默认数组的最后一个元素存储的是第一个节点的下标
int lastIndex = maxSize - 1;
// 获取到待插入的节点的上一个节点的游标
// 首先通过最后一个节点的游标,可以找到第一个节点
// 通过第一个节点的游标,可以找到第二个节点
// 依此类推
for (int i = 1; i <= position - 1; i++) {
lastIndex = data[lastIndex].getCursor();
}
// 设置第0个元素的游标值为待插入节点的游标值,初始化时有设置
data[0].setCursor(data[nextIndex].getCursor());
// 设置这个节点的节点值为待插入值
data[nextIndex].setData(value);
// 设置这个节点的游标为上一个节点的游标值
data[nextIndex].setCursor(data[lastIndex].getCursor());
// 设置上一个节点的游标值为这个节点的位置
data[lastIndex].setCursor(nextIndex);
++length;
}
/**
* 判断是否为空
*
* @return 为空返回true,非空返回false
* @author Korbin
* @date 2023-01-11 15:54:53
**/
public boolean isEmpty() {
return length == 0;
}
/**
* 获取真实长度
*
* @return 长度
* @author Korbin
* @date 2023-01-05 10:15:41
**/
public int length() {
return length;
}
/**
* 查找element在单链表中的位置
*
* @param element 待查找的元素
* @return 元素在单链表中的位置
* @author Korbin
* @date 2023-01-11 15:59:23
**/
public int locateElement(StaticLinkNode<T> element) {
// 依次往下迭代即可
int index = data[maxSize - 1].getCursor();
for (int i = 0; i < length; i++) {
StaticLinkNode<T> node = data[index];
if (node.equals(element)) {
break;
}
index = data[index].getCursor();
}
return index;
}
@Override
public String toString() {
int length = length();
int index = data[maxSize - 1].getCursor();
StringBuilder builder = new StringBuilder("[");
for (int i = 0; i < length; i++) {
StaticLinkNode<T> node = data[index];
builder.append(i + 1).append(":").append(node.getData()).append(", ");
index = node.getCursor();
if (index == 0) {
break;
}
}
builder.append("]");
return builder.toString();
}
}
5.9 静态链表的优缺点
总的来说,静态链表其实是为了给没有指针的高级语言设计的一种实现单链表能力的方法。
6 其他线性表及总结
单链表还可以继续扩展:
(1) 将单链表的终端结点的指针端由空指针改为指向头结点,就使单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表,通过循环链表,可以从任何一个结点出发,访问到链表中的所有结点;
(2) 在单链表的每个结点中,再设置一个指针指向其前驱结点的指针域的链表,称为双向链表,双向链表方便了从某一个结点,获取其前、后结点;
总之,比较常见的线性表包括以下:
注:本文为程 杰老师《大话数据结构》的读书笔记,其中一些示例和代码是笔者阅读后自行编制的。