链表3(LinkedList)

发布于:2025-02-27 ⋅ 阅读:(20) ⋅ 点赞:(0)

1、双向不带头链表的实现

1.1 节点成员和构造方法

        双向不带头链表相比于单向多了一个prev域,它能使链表获得前驱节点。

        如上图是双向不带头链表的一个节点,它可以直接找到前驱和后继节点。

        由上面的讲解可得到代码:(注意:节点的类为静态内部类

//静态内部类  
static class ListNode {
        private int val;//值域
        private ListNode prev;//前驱
        private ListNode next;//后继
        //构造方法
        public ListNode(int val) {
            this.val = val;
        }
    }

        相比于单向链表的头节点,在双向链表中,多了一个尾巴节点last,方便了尾插法。

1.2 size()、display()、contains()

求链表长度、打印链表、查找链表中是否包含该元素

        这三个方法与单链表相同都是通过头节点遍历链表,这里就不做过多赘述。

        代码如下:

 //得到双链表的长度
 public int size(){
            int length = 0;
            ListNode cur = head;
            while(cur != null){
                length++;
                cur = cur.next;
            }
            return length;
        }
 // 打印链表
 public void display(){
            ListNode cur = head;
            while(cur != null){
                System.out.print(cur.val + " ");
                cur = cur.next;
            }
            System.out.println();
        }
    //查找是否包含关键字key是否在单链表当中
        public boolean contains(int key){
            ListNode cur = head;
            while(cur != null){
                if(cur.val == key){
                    return true;
                }
                cur = cur.next;
            }
            return false;
        }

1.3 addFirst()

头插法

        相比于单链表的头插法,双链表的头插法又有什么不同呢?

        如图,要将33头插入当前双链表中,在单链表中,我们只需要绑定后面的元素 ,然后再把头节点给到node,而双链表则还需要改掉原来头节点的前驱。

        由此,有代码如下:

                    node.next = head;
                    head.prev = node;
                    head = node;

        此时,观察代码我们会想:如果我的链表中没有元素,那这里的不就发生空指针异常了吗?

        因此,我们需要判断,当head==null的时候,我们把链表的头节点和尾巴节点都给到node,代码如下:

 if(head == null){
                    head = node;
                    last = node;
                }

        完整代码:

public void addFirst(int data){
                ListNode node = new ListNode(data);
                if(head == null){
                    head = node;
                    last = node;
                }else{
                    node.next = head;
                    head.prev = node;
                    head = node;
                }
        }

1.4 addLast ()

尾插法

                与头插法大同小异,只需要把原来的last的next改为node,再把当前节点的前驱改为last,再让last = node即可,如果链表中没有元素,就把头和尾巴都给到head。 

        完整代码:

   public void addLast(int data){
            ListNode node = new ListNode(data);
            if(head == null){
                head = node;
                last = node;
            }else{
                node.prev = last;
                last.next = node;
                last = node;
            }
        }

1.5 addIndex()

在下标位置插入

   在单链表中,addIndex方法需要找到下标的前一个元素。而在双链表中则可以直接找到该下标的元素(因为我们可以通过前驱来找到前一个元素)。    

          如图所示,我们需要改动4处位置,这里一定要注意这4处的顺序:建议最先改node的prev和next,然后改cur.prev.next,前面的顺序可以改变,但是cur的prev一定是最后改的。 因为是任意位置插入,在0位置我们直接调用头插法函数;在size()位置直接调用尾插法函数;下标不合法,抛异常。

          完整代码: 

//任意位置插入,第一个数据节点为0号下标
        public void addIndex(int index,int data){
            ListNode listNode = new ListNode(data);
            if(index < 0 ||index > size()){
                throw new IndexErrorException(index+"下标错误,无法插入");
            }
            if(index == 0){
                addFirst(data);
                return;
            }
            if(index == size()){
                addLast(data);
                return;
            }
            int count = 0;
            ListNode cur = head;
            while(count != index){
                cur = cur.next;
                count++;
            }
            listNode.next = cur;
            listNode.prev = cur.prev;
            cur.prev.next = listNode;
            cur.prev = listNode;
        }

1.6 remove()

删除第一次出现值为key的节点 

    例如:要删除的key为34。

        因为有了前驱和后继,我们可以直接让cur遍历到值为key节点 ,然后让cur.next.prev = cur.prev; cur.prev.next = cur.next;

         细心的同学此时会发现问题:那我如果要删除的元素是头节点或尾节点,不发生空指针异常了吗?

        是的,所以我们需要写两个if语句进行判断来单独删除头节点和尾巴节点:删除头节点head = head.next; head.prev = null;此外,还应考虑链表只有一个节点的情况:只需将last置空即可。删除尾节点:cur.next.prev = null; last = last.prev;

        代码如下

1.7 removeAllKey()

删除所有值为key的节点

        删除所有值为key的元素,只需要把remove()代码中的return删去,让cur继续走下去即可。

        代码如下:

//删除所有值为key的节点
        public void removeAllKey(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{//只有一个节点的情况
                            last = null;
                        }

                    }else {//删除中间和尾巴
                        //删除尾巴
                        if(cur.next == null){
                            cur.prev.next = cur.next;
                            last = last.prev;

                        }else {//删除中间
                            cur.prev.next = cur.next;
                            cur.next.prev = cur.prev;

                        }
                    }
                }
                cur = cur.next;
            }
        }

 1.8 clear()

清空链表 

         清空链表的所有节点,有两种方法:

1、暴力清空(将head和last直接置空)

public void clear(){
    head = null;
    last = null;
}

 2、将所有节点的前驱和后继置空

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

2、LinkedList和ArrayList的区别

        一、从存储方面:ArrayList在物理和逻辑上都是连续的;LinkedList在物理上不连续,但在逻辑上是连续的。

        二、增删查改和访问元素方面:

        ArrayList在插入的时候需要将被删元素后面的元素后移一个单位,时间复杂度为O(n),而LinkedList只需要直接把节点插进去,时间复杂度为O(1)。此外,ArrayList空间不够需要1.5倍扩容;而链表对容量没有概念,只需要新建一个节点插入即可。

        ArrayList可以通过下标随机访问元素,时间复杂度O(1);LinkedList则只能通过cur一个一个寻找,复杂度为O(n)。

        总结:访问元素次数多的使用顺序表,增删查改次数多则使用链表。