算法通关村第一关——链表青铜挑战笔记

🚈1. 单链表的概念

首先看一下什么是链表?
单向链表就像一个铁链一样,一个链接着一个,环环相扣。

  • 链表是一种在物理上非连续、非顺序的数据结构,由若干节点所组成。

  • 单向链表的每一个节点包含两部分,一部分是存放数据的变量val,另一部分是指向下一个节点的指针next

    image.png

  • 链表的第1个节点被称为头节点,最后1个节点被称为尾节点,尾节点的next指针指向空。

  • 与数组按照下标来随机寻找元素不同,对于链表中的一个节点,我们只能通过头节点(A)的next指针找到该节点的下一个节点(B),再根据节点B的next指针找到下一个节点C……

image.png

  • 单链表有且只有一个后继,可以有多个前驱

图一:
image.png
图二:
image.png

解析:

🌕 图一满足单链表要求,因为链表是要求环环相扣的,核心是一个节点只能有一个后继,但不代表一个节点只能被一个节点指向(可以有多个前驱)。例如:节点c1只有一个后继c2,但有两个前驱a2和b3。
🌕 图二就不满足要求,因为c1有两个后继a5和b4。

注意:
在做题的时候要注意比较的是值还是节点,有时两个节点的值相等,但不是同一个节点。如下图,节点 8 的两个前驱的值相等(都为1),但不是同一个节点。
image.png

🚈2. 链表的定义方式

单链表,一般知道了第一个节点(头节点),就可以通过遍历的方式找到所有节点,所以第一个节点很重要。
image.png
链表定义

链表的节点,由数据域和指针域组成
指针域保存的是下一个节点的地址,即指向下一个节点

// 单链表
    public static class ListNode {
        // 创建节点
        int val; // 数据域 当前节点的值
        ListNode next; // 指针域 指向下一个节点

        // 构造方法
        public ListNode(int val) {
            this.val = val;
            next = null; // 可以省略
        }
    }

🚈3. 链表的基本实现

  1. 链表节点的创建
  2. 链表长度
  3. 打印链表
  4. 插入
  5. 删除

代码整体部分,详细解释请看下文😎

MySingleList 类(主体类):

public class MySingleList {
    // 单链表
    public static class ListNode {
        // 创建节点

        int val; // 数据域
        ListNode next; // 指针域

        // 构造方法
        public ListNode(int val) {
            this.val = val;
            next = null; // 可以省略
        }
    }

    ListNode head;

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

    // 链表打印
    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;
    }

    /**
     * 链表插入,
     * 头部插入、尾部插入、中间插入
     */
    public void addFirst(int val) {
        // 特殊情况,如果head 为 null

        ListNode node = new ListNode(val);
        node.next = head;
        head = node;

    }

    // 尾插
    public void addLast(int val) {
        ListNode node = new ListNode(val);
        if (head == null) {
            head = node;
            return;
        }
        // 找到最后一个结点
        ListNode cur = head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = node;
    }

    public void addIndex(int index, int val) {
        // 判断下标的合法性
        checkPositionIndex(index);
        // 特殊条件放在前面
        if (index == size()) {
            addLast(val);
            return;
        } else if (index == 0) {
            addFirst(val);
            return;
        }
        // 找到下标的前一个元素
        ListNode preNode = findPreIndexNode(index);

        ListNode node = new ListNode(val);
        // 插入,先链接后面的节点

        node.next = preNode.next;
        preNode.next = node;

    }
    private ListNode findPreIndexNode (int index) {
        ListNode cur = head;
        while (index-1 != 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }

    private void checkPositionIndex(int index) {
        if (index < 0 || index > size()) {
            throw new IndexOutOfBoundsException("index非法 " + " Index: "+index+", Size: "+size());
        }
    }

    // 删除中间节点
    public ListNode removeIndex (int index) {
        if (head == null) {
            return null;
        }
        checkElementIndex(index);
        if (index == 0) {
            head = head.next;
            return head;
        }
        // 找到要删除位置的前一个节点
        ListNode preNode = findPreIndexNode(index);

        preNode.next = preNode.next.next;
        return head;
    }

    private void checkElementIndex(int index) {
        if (index < 0 || index >= size()) {
            throw new IndexOutOfBoundsException("index非法 " + " Index: "+index+", Size: "+size());
        }
    }

    //删除第一次出现关键字为key的节点
    public void remove(int key) {
        if (head == null) {
            return;
        }
        // 是否包含key
        if (head.val == key) {
            head = head.next;
        }
        // 找到key节点的前一个节点
        ListNode preNode = findKeyPreNode(key);
        // 没有找到
        if (preNode == null) {
            return;
        }
        preNode.next = preNode.next.next;

    }

    private ListNode findKeyPreNode(int key) {
        ListNode cur = head;
        while (cur.next != null) {
            if (cur.next.val == key) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }
    
}

异常处理类:

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

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

Main方法:
可以根据自己的需求进行编写调试~

public static void main(String[] args) {
        MySingleList mySingleList = new MySingleList();
        MySingleList.ListNode head = mySingleList.head;

        mySingleList.addLast(5);
        mySingleList.addLast(6);
        mySingleList.addIndex(0, 0);
        // mySingleList.addIndex(9, 99);
        mySingleList.display();
        System.out.println(mySingleList.size());
        /*System.out.println("============");
        mySingleList.removeIndex(1);
        mySingleList.display();
        System.out.println(mySingleList.size());*/
    }
🚦3.1 链表遍历

image.png
对于单链表,一定是从头节点开始逐个向后访问,所以操作之后是否还能找到头节点非常重要。切忌把头指针丢了!!!🧐
为了解决“狗熊掰棒子”问题,我们一般另外申请一个变量 cur,使其等于头指针head,后序操作就移动cur就好了~
ListNode cur = head;

// 计算链表的长度
public int size(ListNode head) {
    	// cur 初始时指向头节点 head
        ListNode cur = head;
        int count = 0;

    	// 当链表没有遍历完
        while (cur != null) {
            count++;
            // cur 向后移动一个节点
            cur = cur.next;
        }
        return count;
    } 
🚦3.2 链表插入
  1. 头插

image.png
头插很简单,只需要将新的节点指向表头就行,newNode.next = head;
别忘了head需要重新指向表头!!! head = newNode;

public void addFirst(ListNode head, int val) {
        // 特殊情况,如果head 为 null

        // 创建节点
        ListNode node = new ListNode(val);
        // 将新的节点指向表头
        node.next = head;
        // head指向新的表头
        head = node;

    }
  1. 尾插

image.png
只需要找到链表的尾部,然后将其指向新的节点就ok了~

public void addLast(int val) {
    	// 创建新的节点
        ListNode node = new ListNode(val);

    	// 如果 head = null 说明要插入的节点就是链表的头节点
        if (head == null) {
            head = node;
            return;
        }
        // 找到最后一个结点
        ListNode cur = head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = node;
    }
  1. 中间插入

这里主要介绍中间插入~
注意:index 下标是从0开始

  • 先找到需要插入位置下标的前一个节点,记为 preNode
  • 插入新的节点 node,链接到preNode的后一个节点
  • preNode节点指向新的节点node

🤔思考:为什么先让待插入节点指向后面的节点?

image.png

难点是如何找到要插入的节点的前一个节点?

public void addIndex(int index, int val) {
        // 判断下标的合法性
        checkPositionIndex(index);
        // 特殊条件放在前面
        if (index == size()) {
            addLast(val);
            return;
        } else if (index == 0) {
            addFirst(val);
            return;
        }
        // 找到下标的前一个元素
        ListNode preNode = findPreIndexNode(index);

        ListNode node = new ListNode(val);
        // 插入,先链接后面的节点

        node.next = preNode.next;
        preNode.next = node;

    }

// 找到下标的前一个元素
private ListNode findPreIndexNode (int index) {
        ListNode cur = head;
    	// 只需要让cur向后移动index - 1步
        while (index-1 != 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }
🚦3.3 链表删除

删除同样分为头部删除、中间删除和尾部删除。
🌋注意:当链表本身就为空时,不能删除!

  1. 头删

头删只需要执行 head = head.next 即可。将head向前移动一次
image.png

  1. 尾删

找到删除的节点的前驱节点cur,将其的next指向空 cur.next = null 即可
image.png

  1. 中间删除

只需要找到要删除节点的前一个节点preNode, 找到后,将preNode.next的指针的值更新为preNode.next.next 就可以解决了~
image.png

// 删除中间节点
// index 从0开始
    public ListNode removeIndex (int index) {
    	// 如果链表为空直接返回
        if (head == null) {
            return null;
        }
        // 检查下标的合法性
        checkElementIndex(index);
        // 如果下标为0,头删
        if (index == 0) {
            head = head.next;
            return head;
        }
        // 找到要删除位置的前一个节点
        ListNode preNode = findPreIndexNode(index);

        preNode.next = preNode.next.next;
        return head;
    }

    private void checkElementIndex(int index) {
        if (index < 0 || index >= size()) {
            throw new IndexOutOfBoundsException("index非法 " + " Index: "+index+", Size: "+size());
        }
    }
    
	private ListNode findPreIndexNode (int index) {
	        ListNode cur = head;
	        while (index-1 != 0) {
	            cur = cur.next;
	            index--;
	        }
	        return cur;
	    }

🏝️补充

我们在刷LeetCode的时候,主体函数部分是不需要编写的,只需要写具体函数实现就好了,但是这个对于新手来说很不友好,调试很不方便,下面给一个参考~~

public class Main {
    /**
     * 初始化链表
     * @param array
     * @return
     */
    private static ListNode initLinkedList (int[] array) {
        ListNode head = null, cur = null;

        for (int i = 0; i < array.length; i++) {
            ListNode newNode = new ListNode(array[i]);
            newNode.next = null;
            if (i == 0) {
                head = newNode;
                cur = head;
            } else {
                cur.next = newNode;
                cur = newNode;
            }
        }
        return head;
    }

    /**
     * 将链表的值显示出来
     * @param head
     * @return
     */
    public static String toString(ListNode head) {
        ListNode cur = head;
        StringBuffer stringBuffer = new StringBuffer();
        while (cur != null) {
            stringBuffer.append(cur.val).append(" ");
            cur = cur.next;
        }
        return stringBuffer.toString();
    }

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4};
        ListNode head = initLinkedList(arr);
        /**
         * 这部分是需要调用的函数
         */

        System.out.println(toString(head));
    }

   
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值