顺序表与链表

顺序表

线性表的本质就是数组

手动实现线性表

先定义一下需要实现功能的接口

package List;

public interface IList {
    // 新增元素,默认在数组最后新增
    void add(int data);

    // 在 pos 位置新增元素
    void add(int pos, int data);

    // 判定是否包含某个元素
    boolean contains(int toFind);

    // 查找某个元素对应的位置
    int indexOf(int toFind);

    // 获取 pos 位置的元素
    int get(int pos);

    // 给 pos 位置的元素设为 value
    void set(int pos, int value);

    //删除第⼀次出现的关键字key
    void remove(int toRemove);

    // 获取顺序表⻓度
    int size();

    // 清空顺序表
    void clear();

    // 打印顺序表,注意:该⽅法并不是顺序表中的⽅法,为了⽅便看测试结果给出的
    void display();

}

方法实现

package List;

import java.util.Arrays;

public class MyArrayList implements IList {
    public int[] elem;
    public int usedSize;

    public static final int DEAFULT_CAPACITY = 10;

    public MyArrayList() {
        elem = new int[DEAFULT_CAPACITY];
    }

    public MyArrayList(int initialCapacity) {
        elem = new int[initialCapacity];
    }

    //把数据存放到数据表当中(最后一个位置)
    @Override
    public void add(int data) {
        if (ifFull()) {
            //扩容-》2倍扩容
            grow();
        }
        elem[usedSize] = data;
        usedSize++;
    }

    private void grow() {
        elem = Arrays.copyOf(elem, 2 * elem.length);
    }

    boolean ifFull() {
        return usedSize == elem.length;
    }

    @Override
    public void add(int pos, int data) {
        checkPos(pos);
        if (ifFull()) {
            //扩容-》2倍扩容
            grow();
        }
        //1.移动元素
        for (int i = usedSize - 1; i >= pos; i--) {
            elem[i + 1] = elem[i];
        }
        elem[pos] = data;
        usedSize++;
    }

    private void checkPos(int pos) {
        if (pos < 0 || pos > usedSize) {
            throw new RuntimeException("pos位置不合法:" + pos);
        }
    }

    private boolean isEmpty(){
        return usedSize == 0;
    }

    @Override
    public boolean contains(int toFind) {
        for (int i = 0; i < usedSize; i++) {
            if (elem[i] == toFind) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int indexOf(int toFind) {
        for (int i = 0; i < usedSize; i++) {
            if (elem[i] == toFind) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public int get(int pos) {
        if(isEmpty()){
            throw new ListEmployeeException("获取元素为空");
        }
        checkPos(pos);
        return elem[pos];
    }

    @Override
    public void set(int pos, int value) {
        checkPos(pos);
        elem[pos] = value;
    }

    @Override
    public void remove(int toRemove) {
        int index = indexOf(toRemove);
        if (index == -1) {
            throw new ListEmployeeException("没有这个元素");
        }
        for (int i = index; i < usedSize - 1; i++) {
            elem[i] = elem[i + 1];
            usedSize--;
        }
    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public void clear() {
        usedSize = 0;
    }

    @Override
    public void display() {
        for (int i = 0; i < usedSize; i++) {
            System.out.print(elem[i]+" ");
        }
        System.out.println();
    }
}

package List;

public class ListEmployeeException extends RuntimeException {
    public ListEmployeeException() {
        super();
    }

    public ListEmployeeException(String message) {
        super(message);
    }

    public ListEmployeeException(String message, Throwable cause) {
        super(message, cause);
    }

    public ListEmployeeException(Throwable cause) {
        super(cause);
    }
}

线性表的直接调用

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<>();
        Scanner scanner = new Scanner(System.in);
        int[] arr = {1, 2, 3};
        for (int i = 0; i < 3; i++) {
            int x = arr[i];
            list1.add(x);
        }
        System.out.println(list1);
        System.out.println("========");

        list1.add(1, 6); // 插入元素到指定位置
        System.out.println(list1);

        // 删除指定位置的元素
        if (list1.size() > 2) {
            Integer removedElement = list1.remove(2);
            System.out.println("Removed element: " + removedElement);
            System.out.println("After removal: " + list1);
        }

        // 修改指定位置的元素
        if (list1.size() > 1) {
            list1.set(1, 10); // 将索引1处的元素替换为10
            System.out.println("After update: " + list1);
        }

        // 查找元素是否存在
        boolean containsSix = list1.contains(6);
        System.out.println("List contains 6? " + containsSix);

        // 获取指定元素的索引
        int indexOfTen = list1.indexOf(10);
        System.out.println("Index of 10: " + indexOfTen);
    }
}

单链表

单链表是由一个一个节点组成的

节点的构成

  • 单链表的节点由两部分构成
    • 数值域
    • 指针域

在这里插入图片描述
指针域是用来存放下一个节点的地址的

单链表内部类构建

类似于c/c++的结构体创建链表

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

头插尾差法

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

    ListNode head;

    void addFirst(int data) {
        ListNode node = new ListNode(data);//创建出来新的节点
        node.next=head;
        head = node;
    }

    void addEnd(int data) {
        ListNode node = new ListNode(data);
        if(head == null){
            head=node;
            return;
        }
        ListNode cur=head;
        while(cur.next != null){
            cur=cur.next;
        }
        cur.next=node;
    }
    
    void show() {
         ListNode cur=head;
         while(cur != null){
             System.out.print(cur.val+" ");
             cur=cur.next;
         }
    }

}

在任意位置插入元素

	//在任意位置插入元素
    void addIndex(int index,int data) {
        int len=size();
        if(index < 0 || index > len){
            System.out.println("插入位置不合法");
            return;
        }

        if(index==0){
            addFirst(data);
            return;
        }
        if(index==len){
            addEnd(data);
            return;
        }
        ListNode node=new ListNode(data);
        ListNode cur=searchIndex(index-1);
        node.next=cur.next;
        cur.next=node;
    }

    private ListNode searchIndex(int index) {
        ListNode cur=head;
        int count=0;
        while(count<index){
            count++;
            cur=cur.next;
        }
        return cur;
    }

    //计算链表长度
    private int size() {
        ListNode cur = head;
        int count = 0;
        while(cur.next!=null){
            count++;
            cur=cur.next;
        }
        return count;
    }

插入步骤详解

核心的两行代码:

node.next=cur.next;
cur.next=node;

cur指针移动到了index的位置,注意计数器从count计数,所以真实的位置是index-1。
如果我们想插在指定位置的前面就是这样:

  • 初始状态:
    在这里插入图片描述
    • 注意此时cur指向的是index-1的位置,因为我们想让新节点插入在index之前,让新节点插入到index之后后面解释。
  • 插入节点的核心——先处理“后件”
    在这里插入图片描述
  • node.next=cur.next;
  • cur.next=node;
    • 这样前一个式子的末尾是下一个式子的开头,在链表中操作的cur节点一定是指向需要插入节点的前面的。假如cur在需要插入节点的后面,那单链表就不好操作了,后面双向链表就很很容易解决这样的问题
  • 刚才的问题,如果需要插入的指定位置的后面呢?我们这里就只需要改变ListNode cur=searchIndex(index) 的传入参数的位置就可以了

查找key的前驱节点

//查找key的前驱节点
    public ListNode findNodes(int key){
        ListNode prev=head;
        while(prev.next!=null){
            if(prev.next.val==key){
                return prev;
            }
            prev=prev.next;
        }
        return null;
    }

删除指定节点

在这里插入图片描述

//删除元素
    public void removeAllKeys(int key) {
        if(head==null) return;
        ListNode prev=head;
        ListNode cur = head.next;

        while(cur.next != 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;
        }

    }

返回中间节点

  • 代码一
    //返回中间节点
    public ListNode middleNode() {
        if(head==null) return null;
        if(head.next==null) return head;
        int len=size();
        ListNode cur=head;
        for(int i=0;i<len/2;i++){
            cur=cur.next;
        }
        return cur;
    }
  • 代码二
 //返回中间节点
    public ListNode middleNode() {
        if(head==null){
            return head;
        }
        if(head.next==null){
            return head;
        }
        ListNode fast=head;
        ListNode slow=head;
        //不能互换前后的顺序
        while(fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        return slow;
    }
  1. fast != null && fast.next != null,奇数和偶数的判断条件为什么能放到一起去判断?

    这段代码中 fast != null && fast.next != null 是循环继续的条件,它的作用是确保快指针 fast 能够安全地向后移动两步,同时巧妙地处理了链表长度为奇数和偶数的两种情况。

    条件解释:
    • fast != null:防止快指针已经到达链表末尾时,执行 fast.next.next 出现空指针异常
    • fast.next != null:确保快指针有下一个节点,这样 fast.next.next 才是有效的
    为什么能同时处理奇数和偶数长度:
    1. 当链表长度为奇数时

      • 快指针最终会停在最后一个节点(fast != nullfast.next == null
      • 此时慢指针恰好指向正中间的节点
    2. 当链表长度为偶数时

      • 快指针最终会停在倒数第二个节点的 next(即 fast == null
      • 此时慢指针指向中间两个节点中的第二个节点

    这种处理方式的巧妙之处在于:无论链表长度是奇数还是偶数,循环都会在合适的时机终止,不需要分开处理两种情况。通过一次遍历就能找到中间节点,时间复杂度为 O(n),空间复杂度为 O(1),是寻找链表中间节点的最优解法。

  2. while中的两个判断条件是否可以交换?

    不行,这两个条件的顺序不能交换,否则可能会导致空指针异常(NullPointerException)。

    原因在于逻辑与(&&)运算的短路特性

    • 当判断 fast != null && fast.next != null 时,会先检查 fast != null
      • 如果 fast 已经是 null,则直接跳过第二个条件,不会执行 fast.next 的判断
      • 只有当 fast 不为 null 时,才会去检查 fast.next != null

    如果交换顺序写成 fast.next != null && fast != null

    • fast 已经是 null 时,会先执行 fast.next != null 的判断
    • 此时访问 null.next 就会直接抛出空指针异常

    举个具体例子(偶数长度链表的最后一步):

    • fast 已经指向 D(最后一个节点),此时 fast.nextnull
    • 原条件:fast != null 为真,继续判断 fast.next != null 为假,循环终止(安全)
    • 交换后:先判断 fast.next != null 为假,直接终止(看似安全)

    但再看另一种情况(fast 已经是 null 时):

    • 原条件:先判断 fast != null 为假,直接终止(安全)
    • 交换后:先判断 fast.next != null,此时 fastnull,直接抛出异常(危险)

    因此,这两个条件的顺序是经过精心设计的,必须先判断 fast != null,再判断 fast.next != null,才能保证代码的安全性。

    这种处理方式的巧妙之处在于:无论链表长度是奇数还是偶数,循环都会在合适的时机终止,不需要分开处理两种情况。通过一次遍历就能找到中间节点,时间复杂度为 O (n),空间复杂度为 O (1),是寻找链表中间节点的最优解法。

双向链表

在这里插入图片描述

头插尾插

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

    ListNode head;

    //头插法
    public void addFirst(int data){
        ListNode node=new ListNode(data);

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

    }
    //尾插法
    public void addLast(int data){
        ListNode node=new ListNode(data);
        if(head==null){
            head=node;
        }else{
            ListNode cur=head;
            while(cur.next!=null){
                cur=cur.next;
            }
            cur.next=node;
            node.prev=cur;
        }
    }

双向链表的插入元素

在这里插入图片描述

posted @ 2025-07-25 21:34  dearbi  阅读(0)  评论(0)    收藏  举报  来源