目录
一、ArrayList的缺陷
通过之前的学习,我们已经熟悉了
ArrayList
的使用,并且进行了简单模拟实现。通过源码知道,
ArrayList
底层使用数组来存储元素。
由于其底层是一段连续空间,当
在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n)
,效率比较低,因此
ArrayList不适合做任意位置插入和删除比较多的场景。
因此:
java集合中又引入了LinkedList
,即链表结构。
二、链表
1、链表的概念及结构
链表是一种
物理存储结构上非连续
存储结构,数据元素的
逻辑顺序
是通过链表中的
引用链接
次序实现的 。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向
2. 带头或者不带头
3. 循环或者非循环
虽然有这么多的链表的结构,但是我们重点掌握两种:
无头单向非循环链表:
结构简单
,一般不会单独用来存数据。实际中更多是作为
其他数据结构的子结构
,如哈希桶、图的邻接表等等。另外这种结构在笔试面试
中出现很多。
无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。
2、链表的实现
那么现在,我们可以利用现有的知识,来模拟完成一个链表的实现过程:
public class TwoWayLinkList<T> {
//首结点
private Node head;
//尾结点
private Node last;
//链表长度
private int N;
//结点类
private class Node{
public T item;
public Node pre;
public Node next;
public Node(T item, Node pre, Node next) {
this.item = item;
this.pre = pre;
this.next = next;
}
}
public TwoWayLinkList() {
//初始化头结点和尾结点
this.head = new Node(null,null,null);
this.last = null;
//初始化元素个数
this.N = 0;
}
//清空链表
public void clear() {
this.head.next = null;
this.last = null;
this.N = 0;
}
//获取链表长度
public int length() {
return this.N;
}
//判断链表是否为空
public boolean isEmpty() {
return N == 0;
}
//获取第一个元素
public T getFirst() {
if (isEmpty()) {
return null;
}
return head.next.item;
}
//获取最后一个元素
public T getLast() {
if (isEmpty()) {
return null;
}
return last.item;
}
//插入元素t
public void insert(T t) {
//如果链表为空
if (isEmpty()) {
//创建新结点
Node newNode = new Node(t,head,null);
//让新结点称为尾结点
last = newNode;
//让头结点指向尾结点
head.next = last;
}else {//如果不为空
//创建新结点
Node newNode = new Node(t,last,null);
//让当前尾结点指向新结点
last.next = newNode;
//让新结点成为尾结点
last = newNode;
}
//元素个数加一
this.N++;
}
//指定位置插入元素t
public void insert(int i, T t) {
//找到i位置的前一个结点
Node pre = head;
for (int j = 0; j < i; j++) {
pre = pre.next;
}
//找到i位置的结点
Node cur = pre.next;
//创建新结点
Node newNode = new Node(t,pre,cur);
//让前一结点的next变为新结点
pre.next = newNode;
//让i位置的前一结点变为新结点
cur.pre = newNode;
//元素个数加一
this.N++;
}
//获取指定位置的元素
public T get(int i) {
Node now = head.next;
for (int j = 0; j < i; j++) {
now = now.next;
}
return now.item;
}
//找到第一次出现t的位置
public int indexOf(T t) {
Node now = head;
for (int i = 0; now.next != null; i++) {
now = now.next;
if (now.item.equals(t)) {
return i;
}
}
return -1;
}
//删除i处的元素,并返回其元素
public T remove(int i) {
//找到前一个结点
Node pre = head;
for (int j = 0; j < i; j++) {
pre = pre.next;
}
//找到i位置的下一个结点
Node cur = pre.next;
//找到下一个结点
Node nextNode = cur.next;
//让前一个结点的next指向下一个结点
pre.next = nextNode;
//让下一个结点的pre指向前一个结点
nextNode.pre = pre;
//元素个数减一
this.N--;
return cur.item;
}
}
三、LinkedList的使用
1、什么是LinkedList
关于LinkedList的具体介绍,我们可以看一下它的官方文档;LinkedList 的官方文档
LinkedList的底层是双向链表结构
(
链表后面介绍
)
,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。
在集合框架中,LinkedList也实现了List接口,具体如下:
那么,这里关于LinkedList,我们要注意几点说明:
1. LinkedList 实现了 List 接口2. LinkedList 的底层使用了双向链表3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问4. LinkedList 的任意位置插入和删除元素时效率比较高,时间复杂度为 O(1)5. LinkedList 比较适合任意位置插入的场景
2、LinkedList的使用
(1)LinkedList的构造
(2) LinkedList的其他常用方法介绍
那么,接下来我们来看一下如何使用这些方法:
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(1); // add(elem): 表示尾插
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
System.out.println(list.size());
System.out.println(list);
// 在起始位置插入0
list.add(0, 0); // add(index, elem): 在index位置插入元素elem
System.out.println(list);
list.remove(); // remove(): 删除第一个元素,内部调用的是removeFirst()
list.removeFirst(); // removeFirst(): 删除第一个元素
list.removeLast(); // removeLast(): 删除最后元素
list.remove(1); // remove(index): 删除index位置的元素
System.out.println(list);
// contains(elem): 检测elem元素是否存在,如果存在返回true,否则返回false
if(!list.contains(1)){
list.add(0, 1);
}
list.add(1);
System.out.println(list);
System.out.println(list.indexOf(1)); // indexOf(elem): 从前往后找到第一个elem的位置
System.out.println(list.lastIndexOf(1)); // lastIndexOf(elem): 从后往前找第一个1的位置
int elem = list.get(0); // get(index): 获取指定位置元素
list.set(0, 100); // set(index, elem): 将index位置的元素设置为elem
System.out.println(list);
// subList(from, to): 用list中[from, to)之间的元素构造一个新的LinkedList返回
List<Integer> copy = list.subList(0, 3);
System.out.println(list);
System.out.println(copy);
list.clear(); // 将list中元素清空
System.out.println(list.size());
}
(3)LinkedList的遍历
和其它的遍历差不多,LinkedList的遍历也有三种方法:
1、foreach遍历
for (int e:list) {
System.out.print(e + " ");
}
System.out.println();
2、使用迭代器遍历---正向遍历
ListIterator<Integer> it = list.listIterator();
while(it.hasNext()){
System.out.print(it.next()+ " ");
}
System.out.println();
3、使用反向迭代器---反向遍历
ListIterator<Integer> rit = list.listIterator(list.size());
while (rit.hasPrevious()){
System.out.print(rit.previous() +" ");
}
System.out.println();
当然,还有一种是最常见的循环遍历LinkedList,我们在这里就不赘述了