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

链表青铜挑战

链表是一种最基本的结构,普通的单链表就是只给你一个指向链表头的指针head,如果想访问其他元素,就只能从head开始一个个向后找,遍历链表最终会在访问尾结点之后如果继续访问,就会返回null。

1. 单链表的构造

首先看一下什么是链表?单向链表就像一个铁链一样,元素之间相互连接,包含多个结点,每个结点有一个指向后继元素的next指针。表中最后一个元素的next指向null。如下图:

image.png

1.1 先来构造一个单链表

val 表示 当前元素的值,next 是一个 ListNode 类型,指向下一个节点,像一根链子一样。

public class ListNode {
    public int val;
    public ListNode next;

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

1.2 遍历链表

获取链表长度

思路:从头到尾开始遍历,遍历传来的参数 head,循环访问它的下一个节点,如果有值,计数器 +1,如果为空,返回计数器

/**
     * 遍历链表,返回 长度
     * @param head
     * @return
     */
    public static int getListLength(ListNode head) {
        // 1. 判断链表如果为空,还有下一个为空,返回 0 或 1
        if (head == null) {
            return 0;
        }
        if (head.next == null) {
            return 1;
        }

        int length = 1;
        while (head.next != null) {
            head = head.next;
            length++;
        }
        return length;
    }

打印出链表中每个节点的值

跟上面步骤差不多,循环输出每个节点的值,直到下一个为空

 /**
     * 打印出链表中所有的值
     * @param node
     */
public static void printListNode(ListNode node) {
        while (node != null) {
            System.out.print(node.val + "-->");
            node = node.next;
        }
        System.out.println("NULL");
    }

测试

    @Test
    public void testGetListLength() {
        ListNode listNode1 = new ListNode(1);
        ListNode listNode2 = new ListNode(2);
        ListNode listNode3 = new ListNode(3);


        listNode1.next = listNode2;
        listNode2.next = listNode3;

        System.out.println(listNode1);
        System.out.println("链表长度为:" + ListNode.getListLength(listNode1));
        ListNode.printListNode(listNode1);
    }

 

1.3 链表插入

注意:单链表的插入有三种方式,头部、中部、尾部

(1)头部插入

需要申请一个元素,将 链表的表头插入一个新数据,之前的表头的位置变成第二位。

新增一个 newNode 节点,保存要插入的节点,newNode节点的 next 值 指向 之前的 node,然后返回

/**
     * 头插
     * @param val
     * @param node
     * @return
     */
    public static ListNode addFirst(int val, ListNode node) {
        ListNode newNode = new ListNode(val);
        newNode.next = node;
        node = newNode;
        return node;
    }

(2)尾部插入

跟上面一样,需要申请一个新的节点,将 node 节点遍历到尾部,如果发现下一个元素是 null,那么就这个位置就插入新的节点

/**
     * 尾插
     * @param val
     * @param node
     * @return
     */
    public static ListNode addEnd(int val,ListNode node) {
        ListNode newNode = new ListNode(val);
        ListNode head = node;

        while (node.next != null) {
            node = node.next;
        }
        node.next = newNode;
        return head;
    }

错误注意:

如果最后面返回结果是 node 的话,只会返回最后两个节点,因为在循环结束后,node 的指针已经指向了最后一个节点,然后将新节点添加为最后一个节点的下一个节点。最后,你返回的是node指针,而不是链表的头节点。

因此,要在开始前定义一个节点,记录 node,最后返回这个节点

 

(3)中间插入

思路:

  1. 先对 当前要插入的位置进行判断,如果不符合则报错,为什么这里要 position > length+1,因为可能我要插入到最后一个元素,比如说一个链表1->2->3->4 我想插入第五个,就是插入在 4 的后面变成 1->2->3->4->5,但是如果插入的为 4,等于链表的长度,链表变成  1->2->4->3,所以说判断位置要 +1

  2. 如果当前要插入的位置为 1,或者是 最后 position == length+1,分别调用之前的头插法尾插法就行了

  3. 定义一个 head 节点,用来保存原来 node 的数据

  4. 定义一个 index计数器进行 遍历,从 2 开始,进行计数,循环遍历当前链表,如果该节点位置与要插入的位置相同,那么则直接跳出循环,在后面进行 节点交换(如果都写在 while 循环里,看起来太复杂了,到到这里反正是会遍历到这个 位置的,所以直接 break)

  5. 执行完循环后,就已经确定了要插入的位置,直接新建一个 newNode 节点,使这个节点的 newNode.next = node.next (先前节点的后部分),然后 先前节点前面部分 node.next = newNode

    第 5 步有点不好理解,我画个图        

 代码:

/**
     * 中间插入
     * @param val
     * @param node
     * @param position
     * @return
     */
    public static ListNode addMiddle(int val,ListNode node,int position) {
        // 1. 判断位置是否符合
        int length = ListNode.getListLength(node);
        if (position > length+1 || position < 0) {
            System.out.println("总长度: " + length + "插入的位置不合法!" + position);
        }

        // 判断头插,是否插入在最前
        if (position == 1) {
            node = ListNode.addFirst(val,node);
            return node;
        }
        // 判断尾插,是否插入在最后
        if (position == length+1) {
            node = ListNode.addEnd(val,node);
            return node;
        }

        ListNode head = node;

        // 2. 循环将位置找出来,一旦符合则插入
        int index = 2;
        while (node != null) {
            // 2.1 如果位置符合,则可以跳出循环去执行了
            if (index == position) {
                break;
            }
            node = node.next;
            index++;
        }

        // 现在就是在链表中间了
        ListNode newNode = new ListNode(val);
        newNode.next = node.next;
        node.next = newNode;

        return head;

    }

1.4 链表删除

(1)头删

直接将 表头 指向下一个即可

 	/**
     * 头删
     * @param node
     * @return
     */
    public static ListNode deleteHead(ListNode node) {
        if (node == null) {
            return null;
        }

        return node.next;
    }

(2)尾删

删除链表的最后一个,需要找到尾部,但是不需要找到最后一个,而是要找到倒数第二个,然后让它的 next = null,

node 与 head 指向的是同一块区域,任意修改它们的值都会发生改变,返回的是头节点

 /**
     * 尾删
     * @param node
     * @return
     */
    public static ListNode deleteEnd(ListNode node) {
        if (node == null) {
            return null;
        }
        ListNode head = node;
        // 当下一个为空时停止
        while (head.next.next != null) {
            head = head.next;
        }
        head.next = null;
        return node;

    }

(3)中间删

和之前的 增加中间节点类似,需要找到目标的前一个节点,找到的话 让 头节点的 node.next = node.next.next;

分析具体思路:

  1. index = 2 ,因为之前已经判断过 index == 1了,是否满足头插,这里下标是从 2 开始找

    如果下一个 != null 如果 index == position ,表示已经找到了要插入的位置,跳出循环,执行后面的语句,不符合则后移

  2. 跳出循环后,位置肯定找到了,前面是做了判断,position 不符直接报错,

    node.next = node.next.next;这段代码意思就是 当前节点的下一位 => 当前节点的下一位的下一位(有点抽象,看图)

// 现在删除的节点为中间
        int index = 2;
        while (node.next != null) {
            if (index == position) { // 找到位置,跳出循环
                break;
            }

            index++;
            node = node.next;
        }

        // 找到位置了,跳出循环
        node.next = node.next.next;
        return head;

代码:

/**
     * 中间删
     * @param node
     * @param position
     * @return
     */
    public static ListNode deleteMiddle(ListNode node,int position) {
        if (node == null) {
            return null;
        }

        int length = ListNode.getListLength(node);
        if (position > length || position <= 0) {
            System.out.println("删除位置不正确 " + position);
            return null;
        }

        // 判断是否满足 头插、尾插
        if (position == 1) {
            return ListNode.deleteHead(node);
        }
        if (position == length) {
            return ListNode.deleteEnd(node);
        }

        ListNode head = node;

        // 现在删除的节点为中间
        int index = 2;
        while (node.next != null) {
            if (index == position) { // 找到位置,跳出循环
                break;
            }

            index++;
            node = node.next;
        }

        // 找到位置了,跳出循环
        node.next = node.next.next;
        return head;

    }

2. 双向链表

双向链表顾名思义就是既可以向前,也可以向后。有两个指针的好处自然是移动元素更方便。该结构我们在工程里有大量的应用

其中双向由 prev引用保证,而循环则是头尾节点的相互指向实现的。

 

2.1 构造双向链表

初始化

public class DoubleNode {
    public int data; // 数据
    public DoubleNode next; // 后一个节点
    public DoubleNode pre; // 前一个节点

    public DoubleNode(int data) {
        this.data = data;
    }
    
    /**
     * 打印所有节点信息
     * @param node
     */
    public static void printDoubleNode(DoubleNode node) {
        if (node == null) {
            System.out.println("双向链表为空");
            return;
        }

        while (node.next != null) {
            System.out.print(node.data + "->");
            node = node.next;
        }
        System.out.print("null");
    }
}

测试

// 测试双向链表
    @Test
    public void testDoubleNode() {
        // 初始化双向链表
        DoubleNode doubleNode1 = new DoubleNode(2);
        DoubleNode doubleNode2 = new DoubleNode(4);
        DoubleNode doubleNode3 = new DoubleNode(6);
        DoubleNode doubleNode4 = new DoubleNode(8);

        doubleNode1.next = doubleNode2;
        doubleNode2.pre = doubleNode1;
        doubleNode2.next = doubleNode3;
        doubleNode3.pre =doubleNode2;
        doubleNode3.next =doubleNode4;
        doubleNode4.pre = doubleNode3;

        DoubeNode.printDoubleNode(doubleNode1);

    }

2.2 双向链表插入

(1)头插

在链表头插入,将头节点的上一个节点指向 新节点 node.pre = newNode;

再将新节点的下一个节点指向头节点 newNode.next = node;

 代码:

 /**
     * 头插
     * @param node
     * @return
     */
    public static DoubleNode addHead(int val,DoubleNode node) {
        DoubleNode newNode = new DoubleNode(val);
        if (node == null) {
            return newNode;
        }

        node.pre = newNode;
        newNode.next = node;

        return newNode;
    }

(2)尾插

思路:在双向链表最后插入数据,先定义一个 node变量保存 head信息,用于返回

  1. 先循环遍历到最后,使head指向最后一个节点

  2. 然后 head.next 指向新节点 newNode,然后 newNode.pre 指向 head

/**
     * 尾插
     * @param val
     * @param head
     * @return
     */
    public static DoubleNode addEnd(int val,DoubleNode head) {
        DoubleNode newNode = new DoubleNode(val);
        if (head == null) {
            return newNode;
        }

        DoubleNode node = head;

        // 需要找到尾节点,遍历到最后一个
        while (head.next != null) {
            head = head.next;
        }

        // 此时已经到了最后
        head.next=newNode;
        newNode.pre = head;

        return node;
    }

3)中间插

思路 还是跟上面中间插入单链表差不多,只不过是找到之后 还要 指定 pre 节点

请注意看这一块核心代码:

  1. index = 2 开始找,因为如果是 1 的话就是头插法了,更关键的是可以 提前判断位置,看图吧

int index = 2;
        while (head.next != null) {
            if (index == position) {
                newNode.next = head.next;
                head.next = newNode;
                newNode.pre = head;
            }
            head = head.next;
            index++;
        }

 代码:

/**
     * 中间插入
     * @param val
     * @param head
     * @param position
     * @return
     */
    public static DoubleNode addMiddle(int val,DoubleNode head,int position) {
        DoubleNode newNode = new DoubleNode(val);
        if (head == null) {
            return newNode;
        }

        // 获取长度,判断是否符合
        int length = DoubleNode.getLength(head);
        if (position > length+1 || position <= 0) {
            System.out.println("需要添加的位置不符合条件");
            return null;
        }

        // 判断是否可以 头插、尾插
        if (position == 1) {
            return DoubleNode.addHead(val,head);
        }
        if (position == length+1) {
            return DoubleNode.addEnd(val,head);
        }

        // 现在就是往中间插
        DoubleNode node = head;

        int index = 2;
        while (head.next != null) {
            if (index == position) {
                newNode.next = head.next;
                head.next = newNode;
                newNode.pre = head;
            }
            head = head.next;
            index++;
        }

        return node;
    }

2.3 双向链表删除

(1)头删

头节点的指向 next,然后 使 pre 值为空,因为当前位置已经是头节点了


    /**
     * 删除头节点
     * @param head
     * @return
     */
    public static DoubleNode deleteHead(DoubleNode head) {
        if (head == null) {
            return null;
        }

        head = head.next;
        head.pre = null;
        return head;
    }

(2)尾删

把链表头遍历到倒数第二个,因为最后一个要删除,使 倒数第二个 .next = null

/**
     * 删除尾节点
     * @param head
     * @return
     */
    public static DoubleNode deleteEnd(DoubleNode head) {
        if (head == null) {
            return null;
        }

        DoubleNode node = head;

        // 需要把节点遍历到最后,遍历到倒数第二个,因为最后一个要删除
        while (head.next.next != null) {
            head = head.next;
        }

        head.next = null;

        return node;
    }

(3)中间删

大致思路跟上面 单链表 中间删一致,只不过是多加了几步判定条件

  1. 先判断是否为空,判断删除的下标是否满足,判断是否满足头插、尾插

  2. 从下标 2 开始找,为什么不是1?如果是1的话那么就是满足上面的头删,使 head 节点也后面移动一位

  3. 循环遍历到到删除的节点上

    1. 当前元素的 上一个元素指向的下一个元素 = 当前元素的下一个元素

    2. 当前元素的 下一个元素的 pre(上一个元素) = 当前元素的 上一个     

if (index == position) {
                head.pre.next = head.next;
                head.next.pre = head.pre;
            }

代码:

 /**
     * 删除中间节点
     * @param head
     * @param position
     * @return
     */
    public static DoubleNode deleteMiddle(DoubleNode head,int position) {
        if (head == null) {
            return null;
        }

        // 获取长度,并且判断是否符合
        int length = DoubleNode.getLength(head);
        if (position > length || position <= 0) {
            System.out.println("删除的位置错误!");
            return null;
        }

        // 判断是否满足头删、尾删
        if (position == 1) {
            return DoubleNode.deleteHead(head);
        }
        if (position == length) {
            return DoubleNode.deleteEnd(head);
        }

        DoubleNode node = head;

        /**
         * 从下标 2 开始找,为什么不是1?如果是1的话那么就是满足上面的头删
         * 使 head 节点也后面移动
         */
        int index = 2;
        head = head.next;
        while (head.next != null) {
            /**
             * 如果当前的位置是要删除的位置
             * 1.当前元素的 上一个元素指向的下一个元素 = 当前元素的下一个元素
             * 2.当前元素的 下一个元素的 pre(上一个元素) = 当前元素的 上一个
             */
            if (index == position) {
                head.pre.next = head.next;
                head.next.pre = head.pre;
            }
            index++;
            head = head.next;
        }
        return node;
    }

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值