🎉🎉🎉写在前面:
博主主页:🌹🌹🌹戳一戳,欢迎大佬指点!
博主秋秋:QQ:1477649017 欢迎志同道合的朋友一起加油喔💪
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个小菜鸟嘿嘿
-----------------------------谢谢你这么帅气美丽还给我点赞!比个心-----------------------------
LinkedList与链表
LinkedList与链表
集合类的背后是数据结构,ArrayList的背后是数组,最基本的数据结构。LinkedList的背后是链表,也是数组结构。
一,链表
1.1,链表的概念和结构
链表是一种物理存储上的非连续的存储结构,数据元素的逻辑顺序是依靠引用连接次序来实现的。,链表形象的比喻类似于一个小火车,一节节的串起来。
head就是一个引用类型的变量,用来存储第一个节点的地址。这种存储形式叫做链式存储。
我们的实际中,链表形式有带头的,不带头的,单向的,双向的,循环的,非循环的,所以排列组合后,我们可以得到8种结构。
认识一下什么叫做带头和不带头:
由图可知,带头其实就是有一个专门节点作为头节点,这个头节点不用来存储值,只是用来永远的标识这是这个链表的头位置,不管后面的节点做任何的操作,头节点的位置永远是它不变(也就是head的值不变)。那么不带头其实就是没有一个专门的头节点,它只是默认第一个为头节点,那么在进行操作之后,例如删除节点,那么头节点的位置就会变化,没有一个固定的值(head没有固定值)。
1.2,模拟实现一个单链表集合类
public class SingleList {
static class Node{//内部类,节点类
public int val;
public Node next;//next指向下一个节点
public Node(int val) {
this.val = val;
}
}
public Node head;//存储头结点的引用
public void createList(){
//创建四个节点,只赋值
Node node1 = new Node(10);
Node node2 = new Node(20);
Node node3 = new Node(30);
Node node4 = new Node(40);
//下面把各个节点串起来
node1.next = node2;//node2就是第二个节点的地址
node2.next = node3;
node3.next = node4;
head = node1;
}
}
问题:我们如何去遍历一个链表?
public void displayList(){
while(head != null){
System.out.print(head.val + " ");
head = head.next;
}
}
程序运行结果:
上面虽然确实是已经把链表遍历并且输出了,但是存在一个问题,因为我们是直接动的head指针,所以最终遍历完之后head = null,但是链表中的元素是全都存在的,那你下次再要访问这个链表,但是head等于null,那是不是就有问题了,所以我们不能直接动head,要用一个中间变量去遍历。
【代码改进:】
public void displayList(){
Node cur = this.head;
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;
}
}
上面是一个链表的基本属性,那么下面我们就加上对其的操作方法,形成一个完整的集合类。
public class SingleList {
static class Node{//内部类,节点类
public int val;
public Node next;//next指向下一个节点
public Node(int val) {
this.val = val;
}
}
public Node head;//存储头结点的引用
public void createList(){
//创建四个节点,只赋值
Node node1 = new Node(10);
Node node2 = new Node(10);
Node node3 = new Node(10);
Node node4 = new Node(40);
//下面把各个节点串起来
node1.next = node2;//node2就是第二个节点的地址
node2.next = node3;
node3.next = node4;
head = node1;
}
public void displayList(){
Node cur = this.head;
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//头插法 时间复杂度O(1)
public void addFirst(int data){
Node node = new Node(data);
node.next = this.head;
this.head = node;
// 当你的链表里面没有元素时,头插其实也满足,因为空的时候head = null
};
//尾插法 时间复杂度O(n) 最坏情况下需要找到尾巴
public void addLast(int data){
Node node = new Node(data);
if(this.head == null){
head = node;
}else{
Node cur = this.head;
// 先将cur移动到尾部
while(cur.next != null){
cur = cur.next;
}
cur.next = node;
}
};
//任意位置插入,第一个数据节点为0号下标
private boolean checkIndex(int index){
if(index < 0 || index > size()){
throw new IndexLegalException("插入下标不合法!");
}
return true;
}
private Node findIndexSubOne(int index){
// 找到index-1的节点
Node cur = this.head;
while(index - 1 != 0){
cur = cur.next;
index--;
}
return cur;
}
public void addIndex(int index,int data){
// 其他位置,先检验下标合法性
try{
checkIndex(index);
if(index == 0){
// 那就是头插
addFirst(data);
return;
}
if(index == size()){
// 那就是尾插
addLast(data);
return;
}
// 中间插入
Node cur = findIndexSubOne(index);//找到index - 1位置的节点
Node node = new Node(data);
node.next = cur.next;
cur.next = node;
}catch (IndexLegalException e){
e.printStackTrace();
}
};
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
Node cur = this.head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
};
//删除第一次出现关键字为key的节点
public void remove(int key){
if(this.head == null){
return;
}
if(this.head.val == key){
head = head.next;
return;//必须要return
}
Node cur = searchPrevOfKey(key);
if(cur == null){
return;//cur == null 就说明没有找到这么一个key值节点
}
cur.next = cur.next.next;
};
private Node searchPrevOfKey(int key) {
// 找到key值这个节点的前一个节点
Node cur = this.head;
while(cur.next != null){//遍历链表
if(cur.next.val == key){
return cur;
}
cur = cur.next;
}
return null;
}
//删除所有值为key的节点
public void removeAllKey(int key){
if(this.head == null){
return;//链表为空
}
Node node = new Node(-1);//虚拟节点
node.next = this.head;
Node prev = node;
Node cur = this.head;
// Node prev = this.head;
// Node cur = this.head.next;
while(cur != null){
if(cur.val == key){
cur = cur.next;
prev.next = cur;
}else{
prev = cur;
cur = cur.next;
}
}
this.head = node.next;
//特殊情况,头节点也为key
// if(this.head.val == key){
// head = head.next;
// }
};
//得到单链表的长度
public int size(){
int count = 0;
Node cur = this.head;
while(cur != null){
count++;
cur = cur.next;
}
return count;
};
public void clear(){
this.head = null;//直接把头干掉,那么后续节点就没人引用,就连锁反应,一个个被回收
//也可以遍历,一个个的置为null,但是相对比较麻烦,不是很有必要
};
}
【头插图示解析:】
【尾插图示解析:】
【index位置插入图示解析:】
【删除第一次出现的key:】
【删除所有出现的key:】
三,LinkedList模拟实现
LinkedList背后的数据结构就是一个双向的链表。结构如下:
不得不说,双向链表可真是比单向链表舒服多了,既可以记录尾部的位置,还能记录前驱的地址,不要很强大了!
class 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;//尾节点
//头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
if(head == null){
head = node;//原链表为空的情况下,头节点,尾节点都是node
last = node;
}else{
// 链表不为空的情况下,头插,node的prev为null,不需要考虑
node.next = head;
head.prev = node;
head = node;//把head更新一下,尾节点是不需要更新的
}
};
//尾插法
public void addLast(int data){
ListNode node = new ListNode(data);
if(head == null){//如果刚开始链表就为null
head = node;
last = node;
}else{
last.next = node;
node.prev = last;
last = node;
}
}
private boolean checkAddIndex(int index){
if(index < 0 || index > size()){
throw new IndexException("index不合法!");
}
return true;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
// 先检查index的合法性
try{
checkAddIndex(index);
ListNode node = new ListNode(data);
// 先找到index的元素
if(index == 0){
addFirst(data);
return;//如果index == 0就是头插
}
if(index == size()){
addLast(data);
return;//就是尾插
}
// 中间插入
ListNode cur = head;
while(index != 0){//cur往后走index步,找到index位置的元素
cur = cur.next;
index--;
}
node.next = cur;
node.prev = cur.prev;
cur.prev.next = node;
cur.prev = node;
}catch (IndexException e){
e.printStackTrace();
}
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
ListNode cur = this.head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
ListNode cur = head;
while(cur != null){
if(cur.val == key){
if(cur == head){//如果是删除第一个节点
if(head.next == null){//并且链表就只存在第一个节点
head = null;
}else{
head = head.next;
head.prev = null;
}
}else{//删除的不是第一个节点
if(cur == last) {//是最后一个节点
cur.prev.next = null;
last = cur.prev;
}else{
cur.next.prev = cur.prev;
cur.prev.next = cur.next;
}
}
return;
}
cur = cur.next;
}
}
//删除所有值为key的节点
public void removeAllKey(int key){
// 逻辑就是上面的删除首次出现,只不过就是每次删除了没有直接return掉
ListNode cur = head;
while(cur != null){
if(cur.val == key){
if(cur == head){//如果是删除第一个节点
if(head.next == null){//并且链表就只存在第一个节点
head = null;
}else{
head = head.next;
head.prev = null;
}
}else{//删除的不是第一个节点
if(cur == last) {//是最后一个节点
cur.prev.next = null;
last = cur.prev;
}else{
cur.next.prev = cur.prev;
cur.prev.next = cur.next;
}
}
}
cur = cur.next;
}
}
//得到单链表的长度
public int size(){
ListNode cur = this.head;
int count = 0;
while(cur != null){
count++;
cur = cur.next;
}
return count;
}
public void display(){
ListNode cur = this.head;
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
public void clear(){
ListNode cur = head;
while(cur != null){
ListNode curNext = cur.next;
cur.next = null;
cur.pre = nnull;
cur = curNext;
}
head = null;
last = null;
}
}
【头插图示:】
【尾插图示:】
【index位置插入:】
最后注意一下双向链表的clear()函数不能只单单把head,last置为null就可以了,因为每个节点的pre也算是引用着上一个节点,所以是需要我们遍历然后一个个置为空的,最后再把head与last置为空就完成了整个链表的清空。
四,LinkedList的使用
LinkedList实现了List接口,方法众多。
4.1,LinkedList的多元化使用
我们知道,双向链表的使用是十分普遍的,也正因为它的功能的强大,所以对于LinkedList而言,它不仅仅可以作为双向链表使用,还可以作为栈,队列来使用,具体使用场景后面会介绍。
如此,我们可以发现,在LinkedList的源码中,有很多相似的方法。
由图,我们可以看到,光是插入的方式就多种多样,因为LinkedList作为不同的对象进行使用的时候,方法需要进行区分,尽管都只是对于链表进行操作。
4.2,LinkedList的使用示例
【构造方法】
ArrayList<Integer> arrayList = new ArrayList<>();
LinkedList<Integer> linkedList = new LinkedList<>();//无参构造
LinkedList<Integer> linkedList2 = new LinkedList<>(arrayList);//有参构造
//public LinkedList(Collection<? extends E> c)
有参构造的这种模式在之前的ArrayList里面就介绍过,只要是实现了Collection接口的类,并且类中参数类型只有E和E的子类的类,都可以直接作为构造参数传入LinkedList里面,进行辅助构造。
【其他方法】
class Person{
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
Person tmp = (Person) o;
if(this.name.equals(((Person) o).name) && this.age == ((Person) o).age){
return true;
}
return false;
}
@Override
public String toString() {
return "name:" + this.name + " ; " + "age:"+ this.age;
}
}
public class TestDemo220707 {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(9);
arrayList.add(10);
arrayList.add(20);
LinkedList<Integer> linkedList = new LinkedList<>();//无参构造
linkedList.addAll(arrayList);//尾插arraylist中的所有元素
System.out.println(linkedList);
List ret = linkedList.subList(0,2);
System.out.println(ret);//注意你通过ret是可以修改到linkedlist中的内容的
LinkedList<Person> linkedList1 = new LinkedList<>();
linkedList1.add(new Person("xiaowang",18));
linkedList1.add(new Person("xiaoli",19));
System.out.println(linkedList1);
linkedList1.remove(new Person("xiaoli",19));//Person类需要重写equals方法
System.out.println(linkedList1);
}
}
上面把几个相对于比较难一点的方法进行了示例,至于还有一些比较常规的操作方法,大家可以自行去查看帮助文档,然后进行实操。
【LinkedList的遍历】
public class TestDemo220707 {
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();//无参构造
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
//1,直接输出
System.out.println(linkedList);
//2,foreach循环
for (Integer x:linkedList) {
System.out.print(x + " ");
}
System.out.println();
//3,for循环
for (int i = 0; i < linkedList.size(); i++) {
System.out.print(linkedList.get(i) + " ");
}
System.out.println();
//4,迭代器
Iterator<Integer> it = linkedList.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println();
ListIterator<Integer> it2 = linkedList.listIterator(linkedList.size());//从后往前
while(it2.hasPrevious()){
System.out.print(it2.previous() + " ");
}
System.out.println();
}
}
遍历的方法比较多,这里注重关注一个迭代器的方法, ListIterator it2 = linkedList.listIterator(linkedList.size());当我们传入链表的长度后,我们的迭代器对象会直接来到链表的尾部,开始从后面往前面打印,因为是双向链表,有前驱信息。
五,ArrayList与LinkedList对比
最后,今天的文章分享比较简单,就到这了,如果大家觉得还不错的话,还请帮忙点点赞咯,十分感谢!🥰🥰🥰