【数据结构】第四弹——LinkedList与链表
文章目录
- 一. ArrayList 的缺陷
- 二.链表
- 2.1 链表的概念及结构
- 2.2 链表的结构
- 2.2.1 单向或者双向
- 2.2.2 带头或者不带头
- 2.2.3 循环非循环
- 2.3 链表的实现
- 1. IList接口
- 2. MySingleList 类中具体实现(不带头单向非循环链表)
- 1. 节点抽象成内部类
- 手搓一个链表
- 2. 头插法
- 3. 尾插法
- 4. 指定位置插入
- 5. 是否包含key值
- 6. 删除指定值的节点
- 7. 删除所有指定值所在节点
- 8. 获取链表长度
- 9. 清空链表
- 10. 打印链表
- 三. 链表面试题
- 四. MyLinkedList 的模拟实现(不带头双向非循环链表)
- 节点抽象成内部类
- 1.头插法
- 2.尾插法
- 3.指定位置插入
- 4.是否包含Key值
- 5. 删除指定值的节点
- 6.删除所有指定值的节点
- 7. 获取链表长度
- 8. 清空链表
- 9.打印链表
- 五. LinkedList 的使用
- 5.1 什么是LinkedList
- 5.2 LinkedList 的使用
- 5.2.1 LinkedList 的构造
- 5.2.2 LinkedList的其他常用方法介绍
- 5.3 LinkedList的遍历
- 5.3.1 for遍历
- 5.3.2 foreach
- 5.3.3 迭代器遍历(正向反向)
- 六. ArrayList和LinkedList的区别
一. ArrayList 的缺陷
上篇文章已经熟悉了ArrayList的使用,并且进行了简单模拟实现。通过源码知道,ArrayList底层使用数组来存储元素
ArrayList源码:
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable{// ...// 默认容量是10private static final int DEFAULT_CAPACITY = 10;//...// 数组:用来存储元素transient Object[] elementData; // non-private to simplify nested class access// 有效元素个数private int size;public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}
}
其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java集合中又引入了LinkedList,即链表结构。
二.链表
2.1 链表的概念及结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的
1.从图中可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
2.现实中的节点一般都是从堆上申请出来的
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
2.2 链表的结构
以下情况组合起来就有8种链表结构:
2.2.1 单向或者双向
2.2.2 带头或者不带头
2.2.3 循环非循环
虽然有这么多的链表的结构,但是我们重点掌握两种:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表
2.3 链表的实现
1. IList接口
public interface IList {public void addFirst(int data);//尾插法public void addLast(int data);//任意位置插入,第一个数据节点为0号下标public void addIndex(int index,int data);//查找是否包含关键字key是否在单链表当中public boolean contains(int key);//删除第一次出现关键字为key的节点public void remove(int key);//删除所有值为key的节点public void removeAllKey(int key);//得到单链表的长度public int size();public void clear();public void display();
}
再次回顾:接口中的方法默认都是 public abstract 修饰的,这里不写public也可以
正式使用LinkList 之前还是一样 自己实现一个链表 方便我们更好的理解链表
2. MySingleList 类中具体实现(不带头单向非循环链表)
1. 节点抽象成内部类
手搓一个链表
就是
public void createList() {ListNode node1 = new ListNode(12);ListNode node2 = new ListNode(23);ListNode node3 = new ListNode(34);ListNode node4 = new ListNode(45);ListNode node5 = new ListNode(56);node1.next = node2;node2.next = node3;node3.next = node4;node4.next = node5;this.head = node1;}
2. 头插法
public void addFirst(int data) {ListNode node = new ListNode(data);node.next=head;head=node;}
3. 尾插法
public void addLast(int data) {ListNode node = new ListNode(data);if(head==null){node=head;return ;}ListNode cur =head;while(cur.next!=null){cur=cur.next;}cur.next=node;}
4. 指定位置插入
首先要判断插入的位置是否合法 , 写一个自定义异常
public class IndexException extends RuntimeException{public IndexException(){}public IndexException(String message){super(message);}
}
public void addIndex(int index, int data) {if(index==0){addFirst(data);return ;}int len=size();//获取链表长度if(index==len){addLast(data);}ListNode node=new ListNode(data);ListNode cur=head;try{while(index-1!=0){cur=cur.next;index--;}node.next=cur.next;cur.next=node;}catch (IndexException e){System.out.println("index位置不合法!!!!");e.printStackTrace();}}
5. 是否包含key值
public boolean contains(int key) {//还是需要cur 不能让头结点移动,要不然找不到头了ListNode cur=head;while (cur!=null){if(cur.val==key){return true;}cur=cur.next;}return false;}
6. 删除指定值的节点
public void remove(int key) {ListNode node=new ListNode(key);if(head==null){//空链表 直接return 没啥可删的return;}if(head.val==key){//如果要删的是头 把头定位下一个节点就行了head=head.next;return ;}ListNode cur = findNodeOfKey(key);if(cur==null){return; //没找到要删除的值}ListNode del = cur.next;//要是找到了,就架空del 删除delcur.next=del.next;}public ListNode findNodeOfKey(int key){ListNode cur=head;while (cur.next!=null){if(cur.next.val==key){return cur;}cur=cur.next;}return null;}
7. 删除所有指定值所在节点
public void removeAllKey(int key) {if(head == null) { //头为空 直接返回return;}ListNode prev = head;ListNode cur = head.next;//cur 代表当前节点是否为你要删除的节点while (cur != null) {if(cur.val == key) { //判断一下是否为指定值prev.next = cur.next;cur = cur.next;// 是指定值就 架空cur 删除cur 并且cur向后走一位}else {prev = cur;cur = cur.next; //不是指定值就 两个都往后走}}if(head.val == key) {head = head.next; //头为指定值 删头}}
8. 获取链表长度
public int size() {int len = 0;ListNode cur = head;while (cur != null) {len++;cur = cur.next;}return len;}
9. 清空链表
两种方法: ①直接head置为空 ,有点暴力
②head cur 一个一个置为空, 更温和一点
public void clear() {ListNode cur = head;while (cur != null) {ListNode curN = cur.next;cur.next = null;cur = curN;}head = null;}
10. 打印链表
public void display() {ListNode cur = head;while (cur != null) {System.out.print(cur.val+" ");cur = cur.next;}System.out.println();}
}
三. 链表面试题
关于链表的面试题 都在刷题专栏 test1,像练习链表题的同学可以移步刷题专栏的第一篇文章
以上我们讲的都是不带头单向非循环链表
集合类中都是双向链表,大家注意区分,内部类的实现有所区别,双向链表有三个域,前驱域,数据域,后驱域
四. MyLinkedList 的模拟实现(不带头双向非循环链表)
节点抽象成内部类
static class ListNode{public int val;public ListNode prev;public ListNode next;public ListNode(int val) {this.val=val;}}public ListNode head;public ListNode last;
1.头插法
2.尾插法
public void addLast(int data) {
//尾插 遍历整个链表 最后一个节点的next 指向nodeListNode node=new ListNode(data);if(head==null){head=node;return ;}ListNode cur=head;while(cur.next!=null){cur=cur.next;}cur.next=node;}
3.指定位置插入
public void addIndex(int index, int data) {int len=size();if(index<0 || index>len){System.out.println("index位置不合法");return;}if(index==0){addFirst(data);return ;}if(index==len){addLast(data);return ;}ListNode cur=head;while(index-1!=0){cur=cur.next;index--;}ListNode node=new ListNode(data);node.next=cur.next;cur.next=node;}
4.是否包含Key值
public boolean contains(int key) {ListNode cur=head;while(cur!=null){if(cur.val==key){return true;}cur=cur.next;}return false;}
5. 删除指定值的节点
public void remove(int key) {if(head==null){return ;}//头是需要删除的值 删除 头if(head.val==key){head=head.next;return ;}ListNode cur=findKeyNode(key);ListNode del=cur.next;//cur是需要删除的节点的前一个if(cur==null){return ;//没有需要删除的节点}cur.next=del.next;//架空删除 哪个需要删除的节点}private ListNode findKeyNode(int key){ListNode cur=head;while(cur.next!=null){if(cur.next.val==key){return cur; //获得需要删除的节点的前一个结点 单链表走过了 无法找到前一个结点}cur=cur.next;}return null;}
6.删除所有指定值的节点
public void removeAllKey(int key) {if(head==null){return ;}if(head.val == key) {head = head.next;}ListNode prev=head;ListNode cur=head.next;while(cur!=null){if(cur.val==key){prev.next=cur.next;cur=cur.next;}else{prev=cur;cur=cur.next;}}}
7. 获取链表长度
public int size() {int count=0;ListNode cur=head;while(cur!=null){count++;cur=cur.next;}return 0;}
8. 清空链表
public void clear() {//不是把头置为空 太暴力了 我们一个一个置空ListNode cur=head;while(cur!=null) {ListNode curN = cur.next;cur.next = null;cur = curN;}head=null;}
9.打印链表
public void display() {//打印ListNode cur=head;while(cur!=null){System.out.print(cur.val+" ");cur=cur.next;}System.out.println();}
五. LinkedList 的使用
5.1 什么是LinkedList
LinkList官方文档
LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。
在集合框架中,LinkedList也实现了List接口,具体如下:
【说明】
- LinkedList实现了List接口
- LinkedList的底层使用了双向链表
- LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
- LinkedList在任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
- LinkedList比较适合任意位置插入的场景
5.2 LinkedList 的使用
5.2.1 LinkedList 的构造
public static void main(String[] args) {// 构造一个空的LinkedListList<Integer> list1 = new LinkedList<>();//无参构造List<String> list2 = new java.util.ArrayList<>();list2.add("JavaSE");list2.add("JavaWeb");list2.add("JavaEE");// 使用ArrayList构造LinkedListList<String> list3 = new LinkedList<>(list2);// 使用集合容器中的元素构造}
通配符 (讲解ArrayList的博客中有介绍)list2 由List创建 属于LinkedList 的子类所以可以作为参数
5.2.2 LinkedList的其他常用方法介绍
简单演示几个方法 大家也可以自己点击源码查看其他方法的逻辑
5.3 LinkedList的遍历
5.3.1 for遍历
就是MyLinkedList里的display()方法
5.3.2 foreach
LinkedList<Integer> list = new LinkedList<>();list.add(1); // add(): 默认尾插list.add(2);list.add(3);list.add(4);list.add(5);list.add(6);list.add(7);System.out.println(list.size());// foreach遍历for (int e:list) {System.out.print(e + " ");}System.out.println();
5.3.3 迭代器遍历(正向反向)
// 使用迭代器遍历---正向遍历ListIterator<Integer> it = list.listIterator();while(it.hasNext()){System.out.print(it.next()+ " ");}System.out.println();// 使用反向迭代器---反向遍历ListIterator<Integer> rit = list.listIterator(list.size());while (rit.hasPrevious()){System.out.print(rit.previous() +" ");}System.out.println();