前言
本章用于主要讲述线性表链式存储中 单链表 的介绍与使用,主要使用单链表实现增删改查
链式存储 - 单链表
介绍:
链式存储是物理存储单元上非连续的、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现
规格:
每个元素包含两个结点,一个是存储元素的数据域 (内存空间),另一个是指向下一个结点地址的指针域。根据指针的指向;链表能形成不同的结构,例如 单链表,双向链表,循环链表等。
特点:
- 采用动态存储分配,不会造成内存浪费和溢出
- 链表在插入删除操作相当方便,只需要修改指针即可,不需要移动大量元素
- 由于链表的查找需要从头遍历,越长速度越慢,而且没有下标可用,所有链表的遍历实际上比数组更慢一些
复杂度分析
链表属于常见的一种线性结构,对于插入和移除而言,时间复杂度都为 O(1)
但是对于搜索操作而言,不管从链表的头部还是尾部,都需要遍历 O(n),所以最好复杂度为 O(1) ,最坏的情况就是从头部遍历到尾部才搜索出对应的元素,所以最坏复杂度为 O(n) ,平均复杂度为 O(n)。
前奏准备
本文章讲解所用的编程语言为 Java
为了规范化,分为三个java文件
- Node.java(类):用于定义链表结点
- NodeLink.java(类):用于实现链表方法
- Test(类):程序入口
Node.java
定义结点类Node,一个结点有数据域 data 用于存放结点数据与指针域 next 用于指向下一个结点地址
public class Node {
Object data; //存储的实际数据
Node next;// 指向下一个节点的对象
public Node(Object data) {
this.data = data;
}
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
}
NodeLink.java
接下来到重写java接口的方法类,声明原始数组与原始长度,
public class NodeLink {
// 定义头结点
private Node head;
// 定义长度
private int len;
public NodeLink() {
this.head = null;
this.len = 0;
}
// 方法代码...
}
利用java内置红线报错,Implement Method 进行自动导入方法;当然你也可以手写( ̄▽ ̄)"
Test.java
在我们定义完结点类与方法类后,就可以开启下面的程序入口了
public class Test {
public static void main(String[] args) {
NodeLink node = new NodeLink();
// 运行代码...
}
}
前奏准备完毕后,我们就可以开始重写方法了 (●'◡'●)
insert(插入)
插入有两种,一种是从头插入作为头结点,一种是指定索引插入结点
从头插入方法的思路:
- 进行判断是否符合规范,判断是否从头插入 (健壮性)
- 创建新的结点元素
- 将新结点的指向旧头结点
- 将 head 赋给 新结点
- 更新长度
代码实现
public void insert(int index, Object e) {
/**
* index: 插入的索引
* e: 结点的数据域
*/
// 判断是否符合规范
if (index < 0 || index > len) {
throw new RuntimeException("索引越界");
}
// 从头结点插入
if (index == 0) {
// 1. 创建新的元素节点
Node newHead = new Node(e);
// 2. 将新结点的指向旧头结点
newHead.next = head;
// 3. 赋值头结点
head = newHead;
//*简洁写法,建议将上述理解完后再使用: head = new Node(e, head);
}
// 更新长度
len++;
}
图文解析
Test中跑一下~
node.insert(0, "AAA");
node.insert(0, "BBB");
node.insert(0, "CCC");
指定插入方法的思路:
- 进行判断是否符合规范(健壮性)
- 创建新的结点元素,获取head为前结点
- 循环获取指定下标的前一个结点
- 将新结点的next指向前结点的next
- 前结点的next 指向新结点
- 更新长度
代码示例
public void insert(int index, Object e) {
/**
* index: 插入的索引
* e: 结点的数据域
*/
// 1. 判断是否符合规范
if (index < 0 || index > len) {
throw new RuntimeException("索引越界");
}
// 从头结点插入
if (index == 0) {
...
}
// 从指定索引插入结点
else {
// 2.创建新的结点元素
Node newNode = new Node(e);
// 获取head为前结点
Node curNode = head;
// 3. 获取下标的前一个结点
for (int i = 0; i < index; i++) {
curNode = curNode.next;
}
// 4. 将新结点的next指向前结点的next
newNode.next = curNode.next;
// 5. 将前结点的next 指向新结点
curNode.next = newNode;
}
// 更新长度
len++;
}
图文解析
Test中跑一下~
node.insert(0, "BBB");
node.insert(1, "CCC");
node.insert(1, "AAA");
getNode(获取结点)
用于获取指定结点,多个方法复用;
获取结点方法的思路:
- 进行判断是否符合规范(健壮性)
- 定义头结点 从头开始找
- 循环遍历,因为长度为传入的索引,所以遍历到最后结点就会被找到了
- 返回结点
代码示例
public Object getNode(int index) {
/**
* index: 需要获取的结点索引
*/
if (index < 0 || index >= len) {
throw new IndexOutOfBoundsException("索引越界");
}
// 从头开始找
Node node = head;
// 最后一个就是要找的那个结点索引
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
图文解析
remove(删除)
删除也有两种情况,从头删除与指定索引删除,不过这里合并一起讲
删除方法的思路:
- 进行判断校验(健壮性),判断索引是否超出范围
- 从头删除:将头结点直接赋给下一个结点 (逻辑删除)
- 指定删除:获取要删除结点的前结点,将要删除的结点指向给前结点的next,将前结点的next 指向删除结点的next
- 更新长度
代码示例
public void remove(int index) {
/**
* index: 要删除的结点索引
*/
if (index < 0 || index >= len) {
throw new RuntimeException("索引越界");
}
if (index == 0) {
head = head.next;
} else {
// 1. 创建要删除的结点的 前结点
Node preNode = (Node) getNode(index - 1);
// 2. 将要删除的结点转换为 前结点的下一个结点
Node delNode = preNode.next;
// 3. 将前结点的下一个结点转成要删除的结点的下一个结点
// 相当于直接让要删除的结点失去连接
preNode.next = delNode.next;
}
len--;
}
图文解析
Test中跑一下~
node.remove(0); // 从头删除
node.remove(1); // 指定删除
indexOf(获取索引)
获取指定元素的结点索引
indexof算法的思路:
- 定义头结点 从头开始找,定义
index为当前结点索引 - 循环判断,如果当前结点的值不为 null 就一直遍历 知道找到了或者遍历结束
- 使用
equals与当前结点的数据域与传入的值相匹配 则返回对应索引 - 循环查找过程中 头结点
curNode不停指向下一个,索引随着增加 - 默认返回-1
代码示例
public int indexOf(Object e) {
/**
* e: 要查找的元素
*/
// 从头结点开始查询
Node curNode = head;
int index = 0;
while (curNode != null) {
// 如果当前结点的数据域与传入的值相匹配 则返回对应索引
if (curNode.data.equals(e)) {
return index;
}
// 找不到就换下一个 直到下一个为null
curNode = curNode.next;
index++;
}
return -1;
}
get(获取数据) 与 set(修改结点)
由于前提封装方法获取了结点,减少了很多代码量,这两个方法就可以一起写
get与set方法的思路:
- 获取指定结点
- get 返回指定结点的数据
- set 将指定结点的数据修改为传入的数据
代码示例
public Object get(int index) {
/**
* index: 需要获取的结点索引
* return: 返回结点数据
*/
Node node = getNode(index);
return node.data;
}
public void set(int index, Object e) {
/**
* index: 需要修改的结点索引
* e: 需要修改的结点数据
*/
Node node = getNode(index);
node.data = e;
}
length(长度)
获取链表的长度,直接 返回长度
代码示例
public void length() {
System.out.println("链表长度:" + this.len);
}
display(执行)
用于执行打印所有元素的值
indexof方法的思路:
- 先拷贝头结点,防止冲突
- 循环输出直到结点为空
- 每输出一个结点后换下一个结点
代码示例
public void display() {
// 展示链表结点
Node copy_head = head;
// 1. 循环直到结点为空
while (copy_head != null) {
System.out.println( copy_head.data );
// 2. 打印完后移位下一个
copy_head = copy_head.next;
}
}
打印示例
又到了最后的打印测试,我们来将所有方法测试一下~
以上就是 线性表之链序存储中单链表的定义介绍与方法使用;
ps:链表真是个有趣又好玩的家伙呀 o( ̄▽ ̄)d