一. 链表
1.1 链表的概念
链表也是线性表,是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的
可以看到,链表是在堆上申请的,各对象的地址不一定是连续的
1.2 链表的结构
链表有三种分类:单向/双向,有头/无头,循环/非循环
组合起来一共有八种链表
1.单向/双向
2.有头/无头
3.循环/非循环
本篇文章的主角是无头单向非循环链表/无头双向循环链表
1.3 无头单向非循环链表的实现
下面模拟实现一下无头单向非循环链表
单向链表需要有一个头节点,保存第一个对象的地址
public class SingleList {
//定义结点为链表的内部类
class ListNode {
int val;
ListNode next;
public ListNode(int data) {
this.val = data;
}
}
//保存第一个结点,否则无法找到该链表
ListNode head;
//头插法
public void addFirst(int data){
ListNode newnode=new ListNode(data);
if(head==null) {
head=newnode;
return;
}
newnode.next=head;
head=newnode;
}
//尾插法
public void addLast(int data){
ListNode newnode=new ListNode(data);
if(head==null) {
head=newnode;
return;
}
ListNode cur=head;
while(cur.next!=null) {
cur=cur.next;
}
cur.next=newnode;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
if(index>this.size()||index<0) {
throw new IllegalIndex("插入下标不合法");
}
if(index==0) {
addFirst(data);
return;
}
ListNode cur=head;
index--;
while(index>0) {
cur=cur.next;
}
cur.next=new ListNode(data);
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
ListNode cur=head;
while(cur!=null) {
if(cur.val==key) { //如果要判断的类型为引用类型,要使用equals
return true;
}
}
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
if(head==null) return;
if(head.val==key) {
head=head.next;
return;
}
ListNode cur=head;
while(cur.next!=null) {
if(cur.next.val==key) {
cur.next=cur.next.next;
break;
}
cur=cur.next;
}
}
//删除所有值为key的节点
public void removeAllKey(int key){
if(head==null) return;
while(head!=null&&head.val==key) {
head=head.next;
}
ListNode prev=head,cur=head.next;
while(cur!=null) {
if(cur.val==key) {
prev.next=cur.next;
cur=prev.next;
} else {
prev=cur;
cur=prev.next;
}
}
}
//得到单链表的长度
public int size(){
ListNode cur=head;
int num=0;
while(cur!=null) {
num++;
cur=cur.next;
}
return num;
}
public void clear() {
head=null;
}
public void display() {
ListNode cur=head;
while(cur!=null) {
System.out.print(cur.val+" ");
cur=cur.next;
}
System.out.println();
}
}
//下标不合法异常的定义
public class IllegalIndex extends RuntimeException{
public IllegalIndex() {
}
public IllegalIndex(String message) {
super(message);
}
}
1.4 无头双向非循环链表的实现
下面模拟实现无头双向非循环链表
双向链表需要有头结点和尾结点,方便进行双向遍历
public class MyLinkedList {
class LinkNode {
int val;
LinkNode next;
LinkNode prev;
public LinkNode(int val) {
this.val = val;
}
}
LinkNode head;
LinkNode last;
//头插法
public void addFirst(int data){
LinkNode node=new LinkNode(data);
if(head==null) {
head=last=node;
return;
}
node.next=head;
head.prev=node;
head=node;
}
//尾插法
public void addLast(int data){
LinkNode node=new LinkNode(data);
if(last==null) {
head=last=node;
return;
}
last.next=node;
node.prev=last;
last=node;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
if(index<0) {
throw new IllegalIndex("插入下标不合法");
}
if(index==0) {
addFirst(data);
return;
}
LinkNode prev=head;
index--;
while(index>0) {
if(prev==null) {
throw new IllegalIndex("插入下标不合法");
}
prev=prev.next;
index--;
}
if(prev==null) {
throw new IllegalIndex("插入下标不合法");
}
LinkNode node=new LinkNode(data);
prev.next.prev=node;
node.next=prev.next;
node.prev=prev;
prev.next=node;
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
LinkNode cur=head;
while(cur!=null) {
if(cur.val==key) {
return true;
}
cur=cur.next;
}
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
LinkNode cur=head;
while(cur!=null) {
if(cur.val==key) {
if(cur==head) {
head=head.next;
head.prev=null;
if(head==null)
last=null;
return;
}
if(cur==last) {
last=last.prev;
last.next=null;
if(last==null)
head=null;
return;
}
LinkNode prev=cur.prev;
LinkNode next=cur.next;
prev.next=next;
next.prev=prev;
return;
}
cur=cur.next;
}
}
//删除所有值为key的节点
public void removeAllKey(int key){
LinkNode cur=head;
while(cur!=null) {
if(cur.val==key) {
if(cur==head) {
head=head.next;
if(head==null)
last=null;
return;
}
if(cur==last) {
last=last.prev;
if(last==null)
head=null;
return;
}
LinkNode prev=cur.prev;
LinkNode next=cur.next;
prev.next=next;
next.prev=prev;
}
cur=cur.next;
}
}
//得到链表的长度
public int size(){
LinkNode cur=head;
int len=0;
while(cur!=null){
cur=cur.next;
len++;
}
return len;
}
public void display(){
LinkNode cur=head;
while(cur!=null) {
System.out.print(cur.val+" ");
cur=cur.next;
}
System.out.println();
}
public void clear(){
head=last=null;
}
}
上面clear的实现中使用了比较粗暴的解决方法---直接将头结点和尾结点置空,实际上源码是十分温柔的,分别遍历了每个对象并一一置空
Tip:
下面验证下本博主clear方法究竟实现了没有
你以为我只是简单地打印,当然不是!
在cmd上输入下面的命令,会在d盘上的cookie.txt文件里生成实例对象
这是未使用clear方法时的实例
下面执行一下clear方法,在cookie.txt文件中再次查找LinkNode
二. LinkedList
LinkedList 的底层是双向链表结构 ,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高
从这个框架中可以得出:
1. LinkedList没有实现RandomAccess接口,因此不支持随机访问
2. LinkedList实现了List接口,因此LinkedList也是线性表
3. LinkedList实现了Cloneable接口,因此支持克隆
4. LinkedList实现了Seriallizable接口,因此支持序列化
2.1 LinkedList的使用
1.LinkedList的构造
与ArrayList相同,LinkedList也支持一个实现了Collection接口的对象帮助构造
public static void main(String[] args) { //创造一个ArrayList对象 ArrayList<Integer> array=new ArrayList<>(); array.add(1); array.add(2); array.add(3); //帮助初始化的对象其泛型参数必须和LinkedList一致 LinkedList<Integer> list=new LinkedList<>(array); System.out.println(list); }
输出结果
2. 下面列举LinkedList的常用方法
方法 功能 boolean add (E e)尾插e void add (int index, E element) 将 e 插入到 index 位置 boolean addAll (Collection<? extends E> c) 尾插 c 中的元素 E remove (int index) 删除 index 位置元素 boolean remove (Object o) 删除遇到的第一个 o E get (int index) 获取下标 index 位置元素 E set (int index, E element) 将下标 index 位置元素设置为 element void clear () 清空 boolean contains (Object o) 判断 o 是否在线性表中 int indexOf (Object o)返回O第一次出现的下标 int lastIndexOf (Object o)返回O最后出现的下标 List<E> subList (int fromIndex, int toIndex)截取部分list
3. LinkedList的遍历
因为LinkedList不支持随机访问,LinkedList没有for+下标遍历的用法
1. for-each
public static void main(String[] args) { LinkedList<Integer> link=new LinkedList<>(); for(int i=1;i<=10;i++) link.add(i); for(int e:link) {//增强for循环 System.out.print(e+" "); } }
2. 使用迭代器
迭代器不仅可以实现正序访问,因为双向链表的底层结构,使用迭代器也可以进行反向访问
public static void main(String[] args) { LinkedList<Integer> link=new LinkedList<>(); //给link添加元素 for(int i=1;i<=10;i++) link.add(i); //使用迭代器正向访问 ListIterator<Integer> it= link.listIterator(); while(it.hasNext()){ System.out.print(it.next()+" "); } //使用迭代器反向访问 it= link.listIterator(link.size());//传入下标应为链表大小 while(it.hasPrevious()) { System.out.print(it.previous()+" "); } }
输出结果
为什么要传入链表大小呢?
按照正向迭代器的逻辑逻辑传入的应该是是最后一个元素的下标,即size-1
一起来看看LinkedList的迭代器吧
下面是反向迭代器
总结:
使用正向迭代器时,是包含下标为index的元素的迭代
使用反向迭代器时,不包含下标为Index的元素
下图验证
public static void main(String[] args) { LinkedList<Integer> link=new LinkedList<>(); for(int i=1;i<=10;i++) link.add(i); ListIterator<Integer> it= link.listIterator(0);//传入第一个元素的下标 while(it.hasNext()){ System.out.print(it.next()+" "); //it.remove() } System.out.println(); System.out.println("******************"); it= link.listIterator(link.size()-1);//传入倒数第一个元素的下标 while(it.hasPrevious()) { System.out.print(it.previous()+" "); } }
第一个输出结果包含第一个元素,第二个输出结果不包含最后一个元素
三. ArrayList VS LinkedList
比较项 | ArrayList | LinkedList |
增添 | 只适合尾插,其他的插入需要移动元素 | 不需要移动元素,插入时需要遍历 |
查找 | 可以随机查找 | 需要遍历链表 |
删除 | 只适合删除末尾元素,其他删除需要移动元素 | 不需要移动其他元素,需要遍历链表 |
物理实现 | 物理空间连续,底层是数组 | 物理空间不连续,底层是双向链表 |