手写单链表
Posted LRcoding
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写单链表相关的知识,希望对你有一定的参考价值。
2. 单链表
2.1 单链表技能点
- 存储空间不连续
- 每个结点对应一个数据元素,结点由指针域和数据域组成
- 元素的逻辑关系通过结点间的链接关系体现,逻辑上相邻,物理上不一定相邻
- 按索引查询慢,存储空间大
- 插入、删除速度快
- 有元素才会分配空间,不会有闲置的结点
注:为了对空表、非空表的情况以及首元结点进行统一处理,常增加一个头结点
2.2 手写单链表
测试代码:
public static void main(String[] args) {
//创建线性顺序表
List list = new SingleLinkedList();
//向末尾添加元素
list.add("11111");
list.add("aaaaa");
list.add("bbbbb");
list.add("33333");
list.add("22222");
// 添加到指定位置
list.add(3, "AAAAA");
//进行各种操作验证添加
System.out.println(list.size());
System.out.println(list.isEmpty());
System.out.println(list.get(2));
System.out.println(list.contains("44444"));
System.out.println(list.indexOf("22222"));
System.out.println(list.toString());
}
add 操作及 get 操作的底层思想:
List类:
public interface List {
// 返回线性表的大小,即数据元素的个数。
int size();
// 返回线性表中序号为 i 的数据元素
Object get(int i);
// 如果线性表为空返回 true,否则返回 false。
boolean isEmpty();
// 判断线性表是否包含数据元素 e
boolean contains(Object e);
// 返回数据元素 e 在线性表中的序号
int indexOf(Object e);
// 将数据元素 e 插入到线性表中 i 号位置
void add(int i, Object e);
// 将数据元素 e 插入到线性表末尾
void add(Object e);
// 将数据元素 e 插入到元素 obj 之前
boolean addBefore(Object obj, Object e);
// 将数据元素 e 插入到元素 obj 之后
boolean addAfter(Object obj, Object e);
// 删除线性表中序号为 i 的元素,并返回之
Object remove(int i);
// 删除线性表中第一个与 e 相同的元素
boolean remove(Object e);
// 替换线性表中序号为 i 的数据元素为 e,返回原数据元素
Object replace(int i, Object e);
}
Node类:
public class Node {
Object data;
Node next; // 指向的元素也为结点
public Node() {
}
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Node(Object data) {
this.data = data;
}
// getter / setter / toString方法
}
单链表 SingleLinkedList 类:
public class SingleLinkedList implements List {
private Node head = new Node(); // 事先存在一个头结点
private int size; // 结点数量,默认是0(不包含头结点)
@Override
public int size() {
return size;
}
@Override
public Node get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("索引越界:" + index);
}
// 定义一个 p :指向头结点
Node p = head;
// 循环去找第 index 个结点
for (int i = 0; i <= index; i++) {
p = p.next; // !!!指向下一个结点
}
return p;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void add(int index, Object e) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("索引越界" + index);
}
// p 首先指向头结点
Node p = head;
if (index > 0) {
// 让指针移动到第 index 个结点
p = get(index - 1);
}
// 新建一个结点,并加入数据
Node node = new Node(e);
// 指定该新结点指向下一个结点的地址,也就是p指向的下一个结点的地址
node.next = p.next;
// 修改p指向的下一个结点的地址,为该新结点
p.next = node;
// 结点数量++
size++;
}
@Override
public void add(Object e) {
add(size, e);
}
@Override
public Object remove(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("索引越界" + index);
}
Node p = head;
if (index > 0) {
p = get(index - 1);
}
// 将指向第 index 个结点先存起来
Node currentNode = p.next;
// 第 index 的结点指向下一个结点的地址,直接改为第 index + 1 个结点指向下一个结点的地址
p.next = p.next.next;
// 存起来的结点指向下一个结点的地址,直接指向 null
currentNode.next = null;
size--;
return null;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("[");
/** 直接调用get,相当于频繁的去查找 */
// for (int i = 0; i < size; i++) {
// Node node = get(i);
// builder.append(node.data + ",");
// }
Node p = head;
for (int i = 0; i < size; i++) {
// 顺藤摸瓜
p = p.next; // p 指向第 i个结点
builder.append(p.data + ",");
}
if (size != 0) {
builder.charAt(builder.length() - 1);
}
return builder.toString();
}
}
3. LinkedList
3.1 底层原理
底层是一个双向链表(添加、删除元素效率高,按索引查询效率低)
同时实现了 List、Deque、Queue接口,所以可以当作线性表、队列、双端队列、栈来用
不存在容量不足的问题
3.2 手写LinkedList
LinkedList类:
public class LinkedList implements List {
transient int size = 0; // 有几个结点
transient Node first; // 指向链表的第一个结点(类似于C的指针,并不是结点对象)
transient Node last; // 指向链表的最后一个结点
// 内部类,Node 结点类
private static class Node<E> {
E item; // 数据
Node<E> next; // 指向下一个结点
Node<E> prev; // 指向前一个结点
public Node(Node<E> prev, E item, Node<E> next) {
this.item = item;
this.next = next;
this.prev = prev;
}
@Override
public String toString() {
return "Node{item=" + item + ", next=" + next + '}';
}
}
@Override
public int size() {
return size;
}
@Override
public Object get(int i) {
Node node = node(i);
return node.item;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void add(int i, Object e) {
if (i == size) {
linkLast(e);
} else {
linkBefore(e, node(i));
}
}
/**
* 查询指定索引的结点
* @param index
* @return
*/
private Node node(int index) {
if (index < (size >> 1)) { // 前一半结点,从第一个结点开始向后查询
Node p = first;
for (int i = 0; i < index; i++)
p = p.next;
return p;
} else { // 后一半结点,从后边向前
Node l = last;
for (int i = size - 1; i > index; i--)
l = l.prev;
return l;
}
}
@Override
public void add(Object e) {
linkLast(e);
}
// 在最后面插入数据!!!!
private void linkLast(Object e) {
final Node l = last;
final Node newNode = new Node(l, e, null);
last = newNode;
if (l == null) {
first = newNode;
} else {
l.next = newNode;
}
size++;
}
// 在最前面插入数据
private void linkBefore(Object e, Node succ) {
final Node pred = succ.prev;
final Node newNode = new Node(pred, e, succ);
succ.prev = newNode;
if (pred == null) {
first = newNode;
} else {
pred.next = newNode;
}
size++;
}
}
分析:linkLast
添加结点时的内存分配图
以上是关于手写单链表的主要内容,如果未能解决你的问题,请参考以下文章
JavaLearn#(15)集合提升训练:手写ArrayList单链表LinkedListHashMapHashSet新一代并发集合类
JavaLearn#(15)集合提升训练:手写ArrayList单链表LinkedListHashMapHashSet新一代并发集合类