顺序表(二)

文章目录


前言

上节课已经说明了线性表的顺序结构,这节课我们将继续了解线性结构中链式结构,大家一定很期待吧,快来加入,跟我一起学习吧.

一.什么是链表

链表由一组节点组成,每个节点包含两部分:本节点的数据域和指向下一个节点的指针(或链)。通过这组节点和指针的关系,形成一个链表。
与数组相比,链表的优点在于插入和删除操作简单,不需要移动大量的数据;缺点是不能直接访问任意位置的节点,需要从起始节点开始顺序遍历。链表可用于实现队列和栈等数据结构。

二.线性表的链表结构

2.1链表的初始

所谓的链表就像火车的一节一节的车厢,如下图所示:
在这里插入图片描述
具体的结构就如下图所示:
在这里插入图片描述

2.2 链表的分类

单向或者双向
在这里插入图片描述
带头后者不带头
在这里插入图片描述
循环和非循环
在这里插入图片描述

2.3 单链表

这就是一个经典的链式存储结构.
如果说非要用代码表示的话,我暂时用java代码给大家表示一下,一个链表的具体结构长什么样子:

public class MySingleList {

    static class ListNode {
        public int val;//存储的数据
        public ListNode next;//存储下一个节点的地址

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

    public ListNode head;// 代表当前链表的头节点的引用
    }

在这里插入图片描述

其实我们还是懂了一点小心思的,我们使用了静态内部类,来构建一个完整的类,这样方便我们创建链表的结构.这里全都是以单链表的例子展开的.

2.4 双链表

public 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;
    }

在这里插入图片描述

三.java里面的LinkedList API

3.1 基础操作

操作描述Java实现
boolean add(E e)尾插elist.add(e);
void add(int index, E element)e插入到index位置list.add(index, e);
boolean addAll(Collection<? extends E> c)尾插c中的元素list.addAll(c);
E remove(int index)删除index位置元素list.remove(index);
boolean remove(Object o)删除遇到的第一个olist.remove(o);
E get(int index)获取下标index位置元素list.get(index);
E set(int index, E element)将下标index位置元素设置为elementlist.set(index, element);
void clear()清空list.clear();
boolean contains(Object o)判断o是否在线性表中list.contains(o);
int indexOf(Object o)返回第一个o所在下标list.indexOf(o);
int lastIndexOf(Object o)返回最后一个o的下标list.lastIndexOf(o);
List<E> subList(int fromIndex, int toIndex)截取部分list list.subList(fromIndex, toIndex);
    public static void main(String[] args) {

        LinkedList<Integer> list = new LinkedList<>();
        list.add(1); // add(elem): 表示尾插
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);
        list.add(7);
        System.out.println(list.size());
        System.out.println(list);
// 在起始位置插入0
        list.add(0, 0); // add(index, elem): 在index位置插入元素elem
        System.out.println(list);
        list.remove(); // remove(): 删除第一个元素,内部调用的是removeFirst()
        list.removeFirst(); // removeFirst(): 删除第一个元素
        list.removeLast(); // removeLast(): 删除最后元素
        list.remove(1); // remove(index): 删除index位置的元素
        System.out.println(list);
// contains(elem): 检测elem元素是否存在,如果存在返回true,否则返回false
        if (!list.contains(1)) {
            list.add(0, 1);
        }

        list.add(1);
        System.out.println(list);
        System.out.println(list.indexOf(1)); // indexOf(elem): 从前往后找到第一个elem的位置
        System.out.println(list.lastIndexOf(1)); // lastIndexOf(elem): 从后往前找第一个1的位置
        int elem = list.get(0); // get(index): 获取指定位置元素
        list.set(0, 100); // set(index, elem): 将index位置的元素设置为elem
        System.out.println(list);
// subList(from, to): 用list中[from, to)之间的元素构造一个新的LinkedList返回
        List<Integer> copy = list.subList(0, 3);
        System.out.println(list);
        System.out.println(copy);
        list.clear(); // 将list中元素清空
        System.out.println(list.size());
    }

3.2 链表的遍历操作

分为俩种
foreach遍历
迭代器遍历

    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        list.add(1); // add(elem): 表示尾插
        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();
// 使用迭代器遍历---正向遍历
        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();
    }

四.模拟实现LinkedList的相关操作

我们使用过LinkedList之后,大家一定很好奇它是怎么实现的吧,接下来我们来看看它究竟是怎么一回事.
注意:我们这里全都是以单链表的实现为例子的,

创建一个链表

创建一个链表,我们是以单链表为例子的.

public class MySingleList {

    static class ListNode {
        public int val;//存储的数据
        public ListNode next;//存储下一个节点的地址

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

    public ListNode head;// 代表当前链表的头节点的引用
    }
   

创建链表的方法,我们选择手动创建

  public void createLink() {
        ListNode listNode1 = new ListNode(12);
        ListNode listNode2 = new ListNode(45);
        ListNode listNode3 = new ListNode(23);
        ListNode listNode4 = new ListNode(90);
        listNode1.next = listNode2;
        listNode2.next = listNode3;
        listNode3.next = listNode4; /* */
        head = listNode1;
    }

头插法

头插法的意思顾名思义就是只能从头部插入.

具体的过程就是如下图所示:
在这里插入图片描述
代码展示:

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

尾插法

尾插法是分为俩种情况的,第一种是没有节点,我们直接可以让头结点指向即可.第二种是有节点了,我们需要定义一个节点,让它执行到最后一个节点,让最后一个节点指向新节点.

在这里插入图片描述
代码如下:

    //尾插法 O(N)    找尾巴的过程
    public void addLast(int data){
        ListNode listNode = new ListNode(data);
        if(head == null) {
            head = listNode;
            return;
        }
        ListNode cur = head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = listNode;
    }

任意位置插入,第一个数据节点为0号下标

这里其实是有讲究的,我们需要找到插入的前一个节点,在进行插入操作.
这里就有俩个步骤了.
1.找到前一个节点
2.进行插入.
具体图示如下:

在这里插入图片描述

//任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data)
            throws ListIndexOutOfException{
        checkIndex(index);
        if(index == 0) {
            addFirst(data);
            return;
        }
        if(index == size()) {
            addLast(data);
            return;
        }
        ListNode cur = findIndexSubOne(index);
        ListNode listNode = new ListNode(data);
        listNode.next = cur.next;
        cur.next = listNode;
    }
    private ListNode findIndexSubOne(int index) {
        ListNode cur = head;
        int count = 0;
        while (count != index-1) {
            cur = cur.next;
            count++;
        }
        return cur;
    }
        private void checkIndex(int index) throws ListIndexOutOfException{
        if(index < 0 || index > size()) {
            throw new ListIndexOutOfException("index位置不合法");
        }
    }

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

在这里插入图片描述

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

删除第一次出现关键字为key的节点

删除第一次出现关键词为key的节点,实际上是分为俩个步骤的.
1.查找到要删除的数的前一个节点
2.根据前一个节点找到删除的数
3.删除这个数.
图示如下:
在这里插入图片描述
代码如下:

public void remove(int key){
        if(head == null) {
            return ;//一个节点都没有
        }
        if(head.val == key) {
            head = head.next;
            return;
        }
        ListNode cur = searchPrev(key);
        if(cur == null) {
            return;
        }
        ListNode del = cur.next;//要删除的节点
        cur.next = del.next;
    }
     private ListNode findIndexSubOne(int index) {
        ListNode cur = head;
        int count = 0;
        while (count != index-1) {
            cur = cur.next;
            count++;
        }
        return cur;
    }

删除所有值为key的节点

这个删除所有节点的操作,其实就是想发设发遍历整个链表然后进行删除操作.
1.查找到要删除的数的前一个节点
2.根据前一个节点找到删除的数
3.删除这个数.
4.循环这个操作,直到遍历完整个链表
在这里插入图片描述
代码如下:

 public void removeAllKey(int key){
        if(head == null) {
            return;
        }
        /*while(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;
            }
        }
        if(head.val == key) {
            head = head.next;
        }
    }

得到单链表的长度

这个就是遍历链表的操作,得到最终的长度,我这里就直接给出代码了

 //得到单链表的长度 O(N)
    public int size(){
        int count = 0;
        ListNode cur = head;
        while (cur != null) {
            count++;
            cur = cur.next;
        }
        return count;
    }

打印链表

遍历链表依次进行打印即可.

 public void display() {
        //如果说 把整个链表 遍历完成 那么 就需要 head == null
        // 如果说 你遍历到链表的尾巴  head.next == null
        ListNode cur = head;
        while (cur != null) {
            System.out.print(cur.val+" ");
            cur = cur.next;
        }
        System.out.println();
    }

清空链表

因为有头结点,我把头结点置为空即可.

    public void clear() {
        head = null;

    }

五.链表的相关题目

1. 删除链表中等于给定值 val 的所有节点。OJ链接

这里就不做过多赘述了,这跟我们前面删除定值的val是一样的操作.

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //删除值相同的头结点后,可能新的头结点也值相等,用循环解决
        while(head!=null&&head.val==val){
            head=head.next;
        }
        if(head==null)
            return head;
        ListNode prev=head;
        //确保当前结点后还有结点
        while(prev.next!=null){
            if(prev.next.val==val){
                prev.next=prev.next.next;
            }else{
                prev=prev.next;
            }
        }
        return head;
    }
}

2. 反转一个单链表 OJ链接

这道题十分有意思,我们给出的思路是用头插法进行链表的翻转,具体的图示如下:

在这里插入图片描述

具体代码:

 public ListNode reverseList() {
        if(head == null) {
            return null;
        }
        //说明 只有一个节点
        if(head.next == null) {
            return head;
        }
        ListNode cur = head.next;
        head.next = null;

        while (cur != null) {
            ListNode curNext = cur.next;
            //头插法 插入cur
            cur.next = head;
            head = cur;
            cur = curNext;
        }
        return head;
    }

3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。OJ链接

首先简单的说一下这道题的思路,我们要返回中间节点,这个链表其实是有俩种情况的,第一种就是链表为偶数的时候,另外一种就是链表为奇数的时候.
我这里用图示给大家演示一下:
在这里插入图片描述
另外我们怎么解决这个问题找中间节点的问题呢?我们引入了快慢指针的概念.大家只要记住,快慢指针是一种算法技巧,使用两个指针遍历链表或数组,一个快指针和一个慢指针。它们以不同的速度移动,快指针每次移动两个节点,慢指针每次移动一个节点。通过这两个指针,可以实现对链表的多种操作。
下面我们就来解决这个问题:

在这里插入图片描述
以上就是我门先让fast先走两步,然后再让slow走一步,走到fast为null或者fast.next为null的时候结束.这样奇数个和偶数个都找到了中间节点.
代码如下:

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

4. 输入一个链表,输出该链表中倒数第k个结点。 OJ链接.

OJ链接
这个问题我们还是能用快慢指针解决,你们看我接下来的操作就知道了
1.先让fast走k-1步
2. fast走完之后和slow开始一步一步走
3.当fast.next为空的时候,slow所指的位置就是倒数第K个
具体操作如下图所示:
在这里插入图片描述

代码如下:

 public ListNode findKthToTail(int k) {
        if(k <= 0 || head == null) {
            return null;
        }
        ListNode fast = head;
        ListNode slow = head;
        //1. fast走k-1步
        while (k-1 != 0) {
            fast = fast.next;
            if(fast == null) {
                return null;
            }
            k--;
        }
        //2、3、
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。OJ链接

这道题究竟要我们干什么呢?大家可以看看下面的图片
在这里插入图片描述

我们看到问题就是合并两个有序链表,关键是构建一个新的有序链表,这里的难点是如何同时遍历两个链表。
我给出以下的思路:

  1. 定义两个指针list1、list2分别指向两个链表的头节点head1和head2。
  2. 比较list1和list2指向的节点值,将值较小的节点接到结果链表上,并将相应的指针往后移一节点。
  3. 重复步骤2,直到list1t或者list2到达链表尾部。
  4. 将未到达尾部的链表直接接到结果链表尾部。
  5. 返回结果链表的头节点。

具体的图解如下:
在这里插入图片描述
重复这个步骤即可.

具体的代码操作如下:

 public ListNode Merge (ListNode list1, ListNode list2) {
        // write code here
        ListNode newNode = new ListNode(0);
        ListNode tmp = newNode;
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                tmp.next = list1;
                list1 = list1.next;
            } else {
                tmp.next = list2;
                list2 = list2.next;
            }
            tmp = tmp.next;
        }
        if (list1 != null) {
            tmp.next = list1;
        } else {
            tmp.next = list2;
        }
        return newNode.next;
    }

6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。OJ链接

具体思路:

  1. 定义两个链表bs和as,用于存放值小于x和大于等于x的节点。初始时bs和as都指向null。
  2. 遍历原链表,当节点值小于x时,将该节点添加到bs链表尾部。当节点值大于等于x时,将该节点添加到as链表尾部。
  3. 遍历结束后,bs链表代表值小于x的节点,as链表代表值大于等于x的节点。
  4. 判断bs链表和as链表是否同时不为空。如果bs链表为空,直接返回as链表头部。如果as链表为空,直接返回bs链表头部。
  5. 如果bs链表和as链表同时不为空,则将bs链表尾部节点的next指针指向as链表头部。
  6. 如果as链表不为空,则将as链表尾部节点的next指针指向null。
  7. 返回bs链表头部作为结果。

在这里插入图片描述

具体代码:

public ListNode partition( int x) {
        // write code here
        ListNode bs = null;
        ListNode be = null;
        ListNode as = null;
        ListNode ae = null;

        ListNode cur = head;
        while (cur != null) {
            if(cur.val < x) {
                if(bs == null) {
                    bs = cur;
                    be = cur;
                }else {
                    be.next = cur;
                    be = be.next;
                }
            }else {
                if(as == null) {
                    as = cur;
                    ae = cur;
                }else {
                    ae.next = cur;
                    ae = ae.next;
                }
            }
            cur = cur.next;
        }
        // 有可能不会同时存在小于x 和 大于等于x 的数据
        if(bs == null) {
            return as;
        }
        //第一段不为空
        be.next = as;
        //第2个段为空不为空的问题
        if(as != null) {
            ae.next = null;
        }
        return bs;
    }


7. 链表的回文结构。

判断链表的回文结构,这个题目的思路也很明确,比如我们判断数字的回文结构是怎么判断的,现在只是换到了链表而已.OJ链接
思路如下:

  1. 找到链表的中点,可以使用快慢指针。快指针移动两步,慢指针移动一步,当快指针移动到链表尾部时,慢指针正好在中点。
  2. 反转链表的后半部分。可以从慢指针开始,注意记录前一个节点和后一个节点。
  3. 比较链表前半部分和后半部分的值。前半部分从头节点开始,后半部分从慢指针开始。
  4. 如果全部比较相等,则链表为回文结构。否则不是。
  5. 恢复链表的结构,需要反转后半部分链表。
    在这里插入图片描述

代码如下:

public boolean chkPalindrome() {
        if(head == null) {
            return false;
        }
        if(head.next == null) {
            return true;
        }
        ListNode fast = head;
        ListNode slow = head;
        //1、找中间节点
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        //2、 翻转
        ListNode cur = slow.next;//代表当前需要翻转的节点
        while (cur != null) {
            ListNode curNext = cur.next;
            cur.next = slow;
            slow = cur;
            cur = curNext;
        }
        //3、一个从头往后  一个从后往前
        while (slow != head) {
            if(head.val != slow.val) {
                return false;
            }
            //偶数的情况
            if(head.next == slow) {
                return true;
            }
            slow = slow.next;
            head = head.next;

        }
        return true;
    }

8. 输入两个链表,找出它们的第一个公共结点OJ链接

这里实际的情况就是如下图所示:

在这里插入图片描述
找俩个单链表节点的相交值
1.我们可以定义俩个指针,来指向俩个链表,
2.比较俩个链表的大小
3.让长链表先走k步.
4,然后再一起走,如果相遇,就是相交链表

在这里插入图片描述

具体代码:

  public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        /*
        找俩个单链表节点的相交值
        我们可以定义俩个指针,来指向俩个链表,
        让长链表先走,然后再一起走。
        问题来了,我们就能知道相遇点了
        */
        //首先我们去解决,哪一个更长的问题
        int lenA=0;
        int lenB=0;
        ListNode pl=headA;
        ListNode ps=headB;
        while(pl !=null){
            lenA++;
            pl=pl.next;
        }
         while(ps !=null){
            lenB++;
            ps=ps.next;
        }
        pl=headA;
        ps=headB;
        int len =lenA-lenB;
        if(len <0  ){
            pl=headB;
            ps=headA;
            len=lenB -lenA;
        }
        //然后就知道,那个长,那个短了
        while(len !=0){
            pl=pl.next;
            len--;
        }
        while(pl != ps){
            pl=pl.next;
            ps=ps.next;
        }
        if (pl==ps && pl==null){
            return null;
        }
        return pl;

        
        
    }
}

9. 给定一个链表,判断链表中是否有环。OJ链接

在这里插入图片描述
具体思路:

  1. 使用快慢指针,快指针每次走两步,慢指针每次走一步。
  2. 如果快指针最终走到了链表尾部,说明链表无环,返回false。
  3. 否则快慢指针最终会相遇在环内,返回true。
    为什么我们会使用快慢指针呢?是因为实质上就是我们小学时候使用的追击问题解题思路.我下面将展示一下我的思路.
    在这里插入图片描述

具体代码

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

10.给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL

OJ链接
让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。

具体思路:

  1. 先判断链表是否有环,使用快慢指针可以判断。如果无环,直接返回null。
  2. 如果有环,则将快慢指针重新置于链表头部。慢指针移动一步,快指针移动两步。
  3. 快慢指针会再次相遇,相遇点就是环入口,返回该节点。

为什么快慢指针能找到环入口?

因为从链表头部到环入口的距离等于相遇点到环入口的距离。快指针速度是慢指针的两倍,所以快指针走的距离是慢指针距离的两倍。
当快慢指针在环内相遇后,快指针已经比慢指针多走了一圈环的距离。所以如果从头节点和相遇点各走相同的距离,最终会在环入口相遇。
我来具体的解释一下我的具体思路:
具体思路:
在这里插入图片描述

具体代码:

public ListNode detectCycle() {
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow) {
                break;
            }
        }
        if(fast == null || fast.next == null) {
            return null;
        }
        //
        slow = head;
        while (fast != slow) {
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

忘忧记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值