数据结构之链表(java)

一,链表的概念及结构

1,链表概念

LinkedList(链表)是Java列表中的重要一类,用于对批量数据处理,他在逻辑上连续,在物理上非连续,相比于ArrayList(泛型数组),链表的优势在于更快的增加和删除元素。

2,链表的分类

链表按照有无头部节点,是否构成循环,双向还是单向三个特征(2*3)分为八类

单向链表:

双向链表:

3,链表的结构

a,节点

不管是何种链表,底层都是由一个个相互关联的节点对象组成。

单向链表的节点:

双向链表的节点

b,链表头

在有头链表中,第一个节点前会加一个节点,其next指向第一个节点对象,而其val并不存储元素,这个头是不可改变的,是一个固定的对象。

但是为什么在无头链表中我们也见到有链表"头"这个概念呢?其实这里的头节点并非真正的头,他只是一个引用,总是指向链表第一个对象,这个头并没有一个真正属于自己的对象。

c,循环链表

顾名思义这个链表构成了一个闭环,链表的最后一个节点的next指向了第一个节点。

 

d,下面我们用图描绘一下常见的链表

单向不带头非循环链表

单向带头非循环链表

单向不带头循环链表

双向不带头非循环链表

 

二,链表的模拟实现

以较为常见的单向不可循环链表为例:

首先我们要在自定义链表类下创建一个内部类对象,里面包含了next和var元素,还需要一个头引用 。

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

    public ListNode head;

1,链表初始化

实例化ListNode对象,通过next值串联

public void initList() {
        ListNode listNode1 = new ListNode(1);
        ListNode listNode2 = new ListNode(3);
        ListNode listNode3 = new ListNode(5);
        ListNode listNode4 = new ListNode(6);
        ListNode listNode5 = new ListNode(8);
        ListNode listNode6= new ListNode(9);

       
        listNode1.next = listNode2;
        listNode2.next = listNode3;
        listNode3.next = listNode4;
        listNode4.next=listNode5;
        listNode5.next=listNode6;
        listNode6.next = null;
        this.head = listNode1;
        
    }


​

2,打印链表

通过一个cur引用不断获取下一个节点并打印其val,直到cur为null

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

3,获取链表长度

和打印链表同理

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

4,查找是否存有指定元素,返回boolean值

遍历链表,以指定val作为return条件

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

5,链表头部插入

将新节点的next指向head,再将head前移

public void addFirst(int sum) {
        ListNode node = new ListNode(sum);
        node.next = head;
        head = node;
    }

6,链表尾部插入

遍历到链表尾巴节点,将其next指向新节点

public void addLast(int sum) {
        ListNode node = new ListNode(sum);
        ListNode cur = this.head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = node;
    }

7,指定下标插入元素

如果指定下标再头部或者尾部,直接调用头插法,不是则调用findIndex找到前一个下标进行插入

public void addIndex(int index, int sum) {
        if (index < 0 || index > size()) {
            throw new indexOutOfException("链表下标越界");
        } else if (index == 0) {
            addFirst(sum);
        } else if (index == size()) {
            addLast(sum);
        } else {
            ListNode cur = findIndex(index);
            ListNode node = new ListNode(sum);
            node.next = cur.next;
            cur.next = node;
        }
    }

8,获取指定下标节点的前一个下标

public ListNode findIndex(int index) {
        ListNode cur = this.head;
        int count = 0;
        if (index == 0) {
            throw new RuntimeException();
        }
        while (count != index - 1) {
            cur = cur.next;
            count++;
        }
        return cur;
    }

9,删除指定var节点

找到指定节点的前一个节点,让他指向后一个节点

public void keyRemove(int key) {
        if (head == null) return;

        ListNode cur = head.next;
        ListNode prev = head;
        while (cur != null) {
            if (cur.val == key) {
                prev.next = cur.next;
                cur = cur.next;
                return;
            } else {
                prev = prev.next;
                cur = cur.next;
            }
        }
        if (head.val == key) {
            head = head.next;
        }
    }

10,删除指定val的所有节点

删除一个节点后不return 而是继续遍历寻找

public void removeAllKey(int key) {
        if (head == null) return;
        ListNode cur = head.next;
        ListNode prev = head;
        while (cur != null) {
            if (cur.val == key) {
                prev.next = cur.next;
                cur = cur.next;
            } else {
                prev = prev.next;
                cur = cur.next;
            }
        }
        if (head.val == key) {
            head = head.next;
        }
    }

11,清空链表

为了防止找不到下一个节点,我们需要在将next赋值为null前,找到下一个节点

public void clear() {
        ListNode cur = head.next;
        //ListNode nextNode=cur.next;
        if (cur == null) return;
        while (cur != null) {
            ListNode nextNode = cur.next;
            cur.next = null;
            cur = nextNode;
        }
        head = null;
    }

三,链表面试题

1,反转链表

不断获取下一个节点进行头插,注意为了让cur能找到下一个节点,我们需要一个引用指向cur.next

public ListNode reverseList() {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode cur = head.next;
        head.next = null;
        while (cur != null) {
            ListNode curN = cur.next;
            cur.next = head;
            head = cur;
            cur = curN;
        }
        return head;
    }

2,找到链表的中间节点

可以理解为引用fast的移动速度是slow的两倍,当fast到达终点,slow则指向中间节点

public ListNode findMiddleNode() {
        //ListNode cur=head;
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }

3,删除中间节点

在查找中间节点的过程中记录下slow的前一个节点,以便找到中间节点后删除

public ListNode deleteMiddle() {
        ListNode frontNode = head;
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            frontNode = slow;
            slow = slow.next;
        }
        frontNode.next = slow.next;
        return head;
    }

4,找到倒数第k个节点

引用的移动距离比slow短k-1个节点,当fast到达末端,slow到达倒数第k个节点

public ListNode findToLastNode(int k) {
        if (k <= 0 || k > size()) return null;
        ListNode fast = head;
        ListNode slow = head;
        for (int i = 1; i < k; i++) {
            fast = fast.next;
            if (fast == null) return null;
        }
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

5,合并链表

两个链表各取一个元素进行比较,大的节点接入新链表,并走向下一个节点,重复,直到有一个链表被遍历完,非空部分直接接入新链表

public ListNode mergeTwoLists(ListNode hb) {
        ListNode ha = this.head;
        ListNode headNew = new ListNode(555);
        ListNode tmpH = headNew;
        while (ha != null && hb != null) {
            if (ha.val <= hb.val) {
                tmpH.next = ha;
                tmpH = tmpH.next;
                ha = ha.next;
            } else {
                tmpH.next = hb;
                tmpH = tmpH.next;
                hb = hb.next;
            }
        }
        if (ha != null) {
            tmpH.next = ha;
        }
        if (hb != null) {
            tmpH.next = hb;
        }
        return headNew.next;
    }

6,指定元素,大小元素分居两侧

引用bs和be指向较小元素节点的左右两as和ae指向较大元素节点的左右两侧,链表遍历完后合并be和as

public ListNode partition(int x) {
        ListNode cur = head;
        ListNode bs = null;
        ListNode be = null;
        ListNode as = null;
        ListNode ae = null;
        if (cur == null) {
            return null;
        }
        while (cur != null) {
            if (cur.val <= x) {
                if (bs == null) {
                    bs = cur;
                    be=cur;
                } else {
                    be.next = cur;
                    be = cur;
                }
            } else {
                if (as == null) {
                    as = cur;
                    ae=cur;
                }
                else {
                    ae.next = cur;
                    ae = cur;
                }
            }
            cur=cur.next;
        }
        be.next = as;
        if(as!=null){
            ae.next = null;
        }
        return bs == null ? as : bs;
    }

 

7,判断回文链表(即元素轴对称)

先找到中间节点,然后反转后半段链表,从head和slow这两个引用同时遍历链表

,在两者相遇之前如果一直满足head.var==slow.var则为回文链表

public  boolean chkPalindrome(){
        ListNode fast=head;
        ListNode slow=head;
        while (fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        ListNode cur=slow.next;
        while (cur!=null){
            ListNode curN=cur.next;
            cur.next=slow;
            slow=cur;
            cur=curN;
        }
        while (head!=slow){
            if(head.val!=slow.val){
                return  false;
            }
            if(head.next==slow) return  true;
            head=head.next;
            slow=slow.next;
        }
        return  true;
    }

8,找到两个链表共同节点

"截"掉长链表的前一部分,使两链表长度相等,同时遍历进行比较,直到找到相同节点或者遍历完链表

public ListNode getIntersectionNode(){
        int size1=size();
        int size2=size2();
        int sizeMin=size1<=size2?size1:size2;
        int sizeMax=size1<=size2?size2:size1;
        ListNode cur1=size1<=size2?headB:head;
        ListNode cur2=size1<=size2?head:headB;
        while ((sizeMax-sizeMin)>0){
            cur1=cur1.next;
            sizeMax--;
        }
        showNode(cur1);
        showNode(cur2);
        while (cur2.next!=null){
            if(cur1==cur2){
                break;
            }
            cur1=cur1.next;
            cur2=cur2.next;
        }
        if(cur2.next!=null){
            return cur1;
        }
        else {
            return null;
        }
    }

9,找到循环链表的循环点

先判断是否为循环链表,使用快慢指针,若指针相遇则为循环链表,进行查找:

如图c点为指针相遇点,设慢指针路程为s,快指针路程为2*s

则s=ab+bc,2*s=ab+bc+cd+bc,可以解得ab=cb,因此只需要从a点和c点同时遍历链表,当两个引用相遇,即为循环点

public ListNode findRingBegin(){
        ListNode cur1=head;
        ListNode cur2=head;
        if(head==null) return  null;
        while (cur1.next!=null&&cur1!=null){
            cur1=cur1.next.next;
            cur2=cur2.next;
            if(cur1==cur2) {
                ListNode cur=head;
                ListNode meet=cur1;
                while (cur!=meet){
                    cur=cur.next;
                    meet=meet.next;
                }
                return cur;
            }
        }
        return  null;
    }

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值