LinkedList与链表

1.链表的概念与结构

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。

在这里插入图片描述
【注意】:

  1. 上图画的链表结构中其中的一种:不带头结点-单向非循环的链表
  2. 从上图可以看出,链式结构在逻辑上是连续的,但在物理空间上不一定连续
  3. 现实中的节点一般都是从堆上申请出来的

除了上面这种结构,链表还有很多其他的结构
以下情况组合起来就有8种链表结构:
(1)单向或双向
在这里插入图片描述(2)带有节点或不带头结点
在这里插入图片描述
(3)循环或非循环
在这里插入图片描述

虽然链表的结构有很多种,但我们使用最多的是下面的这两种

  • 不带头单向非循环链表 :结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
    +
  • 不带头的双向链表: 在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。

2.链表的实现

(1)定义链表中的节点
👂

class ListNode{
    public  int value; //数值域
    public ListNode next;//存储下一个节点的地址
    public ListNode(int value) {
        this.value = value;
    }
}

(2)定义链表这个类

public class MySingleList {

      public  ListNode head; //单链表的头节点的引用
      //单链表的头节点是相对于整个链表而言的,不能在节点的类中
 }

下面的这些方法都是定义在单链表这个类中的

(3)打印链表
💌

 public void display(){
       ListNode cur=this.head;
       while(cur!=null){
           System.out.print(cur.value+" ");
           cur=cur.next;
       }
        System.out.println();
    }

(4)查找是否包含关键字key是否在单链表当中
👥

    public boolean contains(int key){
        
       ListNode cur=this.head;
       while (cur!=null){
           if(cur.value==key){
               return true;
           }
           cur=cur.next;
       }
       return false;
    }

(5)得到单链表的长度
👮

 public int size(){
       ListNode cur=this.head;
       int size=0;
       while (cur!=null){
           size++;
           cur=cur.next;
       }
        return size;
    }

(6)头插法 (时间复杂度O(1))
在链表头部插入一个新的节点,并让链表的头(head)指向这个新节点
在这里插入图片描述

👸

 public void addFirst(int data){
        ListNode listNode=new ListNode(data);
           listNode.next=this.head;
           this.head=listNode;
    }

【注意】:
listNode.next=this.head;
this.head=listNode;
这两步的顺序不可以颠倒

(7)尾插法 (时间复杂度O(N))
在链表的尾部插入一个新的节点
💦

   public void addLast(int data){
      ListNode listNode=new ListNode(data);
      if(this.head==null){       //判空操作
          this.head=listNode;   //如果链表为空,直接用head引用要插入的新节点
      }  else {
          ListNode cur=this.head;
          while (cur.next!=null){
              cur=cur.next;          //找到链表的最后一个节点
          }
          cur.next=listNode;      //在尾节点后插入新的节点
      }
      
    }

【注意】:
这里while()循环结束的条件是cur.next!=null
因为cur.nxet==null 时,cur指向的是链表的最后一个节点

(8)任意位置插入,指定第一个数据节点为0号下标
在这里插入图片描述

💚

        //检查index的合法性
   private void checkIndex(int index){
       if(index<0||index>size()){
           throw  new IndexOutOfException("Index 不合法");
       }
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
             checkIndex(index);//判断index的合法性
             ListNode listNode=new ListNode(data);
             if(index==0){  //index==0,相当于头插
                 addFirst(data);
                 return ;
             }
             if(index==size()){//index==size,相当于尾插
                 addLast(data);
                 return ;
             }
            ListNode cur=this.head;
             while ((index-1)!=0){
                 cur=cur.next;
                 index--;//让cur指向要插入位置节点的前一个节点
             }

             listNode.next=cur.next;
             cur.next=listNode; 
               return ;
    }

(9)删除第一次出现关键字为key的节点
💢

public void remove(int key){
       if(this.head==null){
           System.out.println("链表为空!!!!");
           return ;         //如果链表为空,直接返回
       }
       if(this.head.value==key){
           this.head=this.head.next;
           return;        //如果第一个节点就是要删除的节点,直接让头节点指向下一个节点
       }
       ListNode cur=this.head;
       while(cur.next!=null){  
           if(cur.next.value==key){//让cur指向要删除节点的前一个节点
               ListNode del=cur.next;
               cur.next=del.next;
               return ;
           }
           cur=cur.next;
       }
    }

【注意】:
如果想要删除头节点需要特殊考虑

3.LinkedList的模拟实现

LinkedList底层就是一个双向链表,在上面我们已经实现了不带头的单链表的各种功能,下面我们来实现一个双向链表

对于双向链表的节点有三个域:数值域、前驱和后继

class ListNode{
    public int val;
    public ListNode prev; //前驱
    public ListNode next; //后继

    public ListNode(int val) {
        this.val = val;
    }
}

在MyLinkList这个类中有两个属性,head和last ,分别用来标记链表的头和尾

public class MyLinkList {
    
    public  ListNode head; //标记链表的头
    public ListNode last; //标记链表的尾
    
    
}

双向链表的功能

(1)打印双向链表
和单链表的打印一样,只用后继域就可以了

 public void display(){
      ListNode cur=this.head;
      while (cur!=null){
          System.out.print(cur.val+" ");
          cur=cur.next;
      }
        System.out.println();
    }

(2) 头插法

在这里插入图片描述

 public void addFirst(int data){

        ListNode listNode=new ListNode(data);
        if(this.head==null){
            this.head=listNode;
             this.last=listNode;
        }else {
            listNode.next=this.head;
            this.head.prev=listNode;
            this.head=listNode;
        }

    }

(3)尾插法
相比较单链表的尾插,双向链表的尾插要更容易,单向链表进行尾插,需要遍历找到尾巴,而双向链表有指向尾巴的last
在这里插入图片描述

public void addLast(int data){

        ListNode listNode=new ListNode(data);
        if(this.head==null){
            this.head=listNode;
            this.last=listNode;
        }else {
            this.last.next=listNode;
            listNode.prev=this.last;
            this.last=listNode;
        }
    }

(4)任意位置的插入
在这里插入图片描述

 public void addIndex(int index,int data){
        if(index < 0 || index > size()) {
            System.out.println("index不合法!");
            return;
        }
        if(index == 0) {
            addFirst(data);
            return;
        }
        if(index == size()) {
            addLast(data);
            return;
        }
        //cur拿到了index下标的节点的地址
        ListNode cur = searchIndex(index);
        ListNode node = new ListNode(data);
        node.next = cur;
        cur.prev.next = node;
        node.prev = cur.prev;
        cur.prev = node;
    }
    
    private ListNode searchIndex(int index) {
        ListNode cur = head;
        while (index != 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }

(5)判断链表中是否包含某个节点

 public boolean contains(int key){
        ListNode cur = head;
        while (cur != null) {
            if(cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

(6)删除第一次出现关键字为key的节点
考虑多种情况:
1.要删除的节点是头节点
2.要删除的节点是尾节点
3.中间节点
4.当链表中只有一个节点并要删除这个节点时
在这里插入图片描述

  //删除第一次出现关键字为key的节点
    public void remove(int key){
        ListNode cur=head;
        while (cur!=null){
            if(cur.val==key){
                if(cur==head){
                    head=head.next;
                    if(head!=null){
                        head.prev=null;
                    }
                }else {
               cur.prev.next=cur.next;
               if(cur.next!=null){
                   cur.next.prev=cur.prev;
               }else {
                   last=last.prev;
               }
                }
      return;
            }else{
                cur=cur.next;
            }
        }
    }

(7)删除所有值为key的节点
只需对上面的删除第一次出现关键字为key的节点,做一点修改就可,
在这里插入图片描述
(8)清空链表

源码:

  public void clear(){
     ListNode cur=head;
     while(cur!=null){
         ListNode curNext=cur.next;
         cur.prev=null;
         cur.next=null;
         cur=curNext;
     }
    this.head=this.last=null;
    }

【注意】:
上面的操作方法都是针对于无头的双向链表实现的
如果想实现有头的双向链表,只需在构造方法中,创建一个节点,并让head和last都引用它

    public MyLinkList(){
        this.head=this.last=new ListNode(-1);
    }

对于带头节点的头插法,都要插到头结点的后面

4.LinkedList的使用

4.1LinkedList的基本概念

LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。

在集合框架中,LinkedList也实现了List接口,具体如下:
在这里插入图片描述
说明:

  1. LinkedList实现了List接口
  2. LinkedList的底层使用了双向链表
  3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
  4. LinkedList的头插、头删、尾插、尾删时效率比较高,时间复杂度为O(1)
  5. LinkedList的中间位置插入的时间复杂度是O(N)

4.2LinkedList的常用方法

4.2.1 构造方法

在这里插入图片描述

    public static void main(String[] args) {
        LinkedList<Integer> linkedList=new LinkedList<>();
        linkedList.add(1);
        linkedList.add(2);
        LinkedList<Integer> linkedList1=new LinkedList<>(linkedList);
        linkedList1.add(888);
        System.out.println(linkedList1);
    }

4.2.2其他常用方法

在这里插入图片描述

    public static void main(String[] args) {
        LinkedList<Integer> linkedList=new LinkedList<>();
        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);
        linkedList.add(6);
        linkedList.add(7);
        LinkedList<Integer> linkedList1=new LinkedList<>(linkedList);
        linkedList1.add(888);
        System.out.println(linkedList1);
        linkedList.remove(4);
        System.out.println(linkedList.get(3));
        System.out.println(linkedList.contains(6));
        System.out.println(linkedList.indexOf(6));

    }

4.2.3LinkedList的遍历

(1) 使用for-each

   public static void main(String[] args) {
        LinkedList<Integer> linkedList=new LinkedList<>();
        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);
        linkedList.add(6);
        linkedList.add(7);

        for (int x:linkedList) {
            System.out.print(x+" ");
        }
        System.out.println();

(2) 使用迭代器

正序打印

   public static void main(String[] args) {
        LinkedList<Integer> linkedList=new LinkedList<>();
        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);
        linkedList.add(6);
        linkedList.add(7);

        for (int x:linkedList) {
            System.out.print(x+" ");
        }
        System.out.println();
        System.out.println("============");
        Iterator<Integer> it=linkedList.iterator();
        while (it.hasNext()){
            System.out.print(it.next()+" ");
        }
        System.out.println();
  }

倒序打印

  ListIterator<Integer> listIterator= linkedList.listIterator(linkedList.size());
        while (listIterator.hasPrevious()){
            System.out.print(listIterator.previous()+" ");
        }
        System.out.println();

在倒序打印时,要给定链表的大小

5.ArrayList和LinkedList的区别

  1. 存储方式
    ArrayList是一块连续的内存,在物理上和逻辑上都是连续的
    LinkList 物理上不一定连续,逻辑上连续
  2. 增删查改的区别
    增加和删除元素时,建议使用LinkList,只需要修改指向就可以O(1),而ArrayList需要挪动数据O(N)
    通过下标进行查找修改元素时,建议使用ArrayList O(1),LinkList是O(N)
  3. ArrayList支持随机访问O(1),LinkList不支持
  4. ArrayLlis空间不够时需要扩容,LinkList没有容量的概念

6.单链表经典面试题

十道经典面试题详解

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值