算法通关村第二关[白银挑战]-链表反转

算法通关村第二关[白银挑战]-链表反转

  • 1.指定区间反转(LeetCode92)
    • 1.1 头插法
    • 1.2 穿针引线法
  • 2.两两交换链表中的结点(LeetCode24)
  • 3.单链表+1(LeetCode369)
    • 3.1 使用栈进行反转
    • 3.2 使用链表进行反转
  • 4.链表加法(LeetCode445)
    • 4.1 用栈实现
    • 4.2 使用链表反转实现
  • 5.再论链表回文序列的问题

1.指定区间反转(LeetCode92)

算法通关村第二关[白银挑战]-链表反转_第1张图片
图示:
算法通关村第二关[白银挑战]-链表反转_第2张图片

1.1 头插法

在这里插入图片描述
反转的整体思想是,在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。

下图展示了整个流程:
算法通关村第二关[白银挑战]-链表反转_第3张图片

    /**
     * 方法1:头插法
     *
     * @param head
     * @param left
     * @param right
     * @return
     */
    public ListNode reverseBetween(ListNode head, int left, int right) {
            ListNode dummyNode=new ListNode(-1);
            dummyNode.next=head;
            ListNode pre=dummyNode;
            //遍历pre结点到left位置结点的前一个结点
            for(int i=0;i<left-1;i++){
                pre=pre.next;
            }
            //当前结点记录left位置结点
            ListNode cur=pre.next;
            ListNode next;
            //遍历次数就是right-left次
            for(int i=0;i<right-left;i++){
            	//1.先记录下一个位置的结点
                next=cur.next;
                //2.让当前结点指向下一个结点的下一个结点
                //随着下一个结点被移到left位置,会让当前结点不断指向再下一个结点
                cur.next=next.next;
                //3.让下一个结点指向left位置的结点
                next.next=pre.next;
                //4.让pre指向next结点
                //3.4让下一个结点移到left位置
                pre.next=next;
            }
            return dummyNode.next;
    }

这是每次循环的具体步骤:
算法通关村第二关[白银挑战]-链表反转_第4张图片

1.2 穿针引线法

算法通关村第二关[白银挑战]-链表反转_第5张图片

先确定好需要反转的部分,也就是下图的 left 到 right 之间,然后再将三段链表拼接起来。这种方式类似裁缝一样,找准位置减下来,再缝回去。这样问题就变成了如何标记下图四个位置,以及如何反转left到right之间的链表。

算法通关村第二关[白银挑战]-链表反转_第6张图片

算法步骤:
第 1 步: 先将待反转的区域反转
第 2 步:把 pre 的 next 指针指向反转以后的链表头节点,把反转以后的链表的尾节点的
next 指针指向 succ。
算法通关村第二关[白银挑战]-链表反转_第7张图片

    /**
     * 方法2:穿针引线法
     *
     * @param head
     * @param left
     * @param right
     * @return
     */
    public ListNode reverseBetween(ListNode head, int left, int right) {
    // 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
            ListNode dummyNode=new ListNode(-1);
            dummyNode.next=head;
            ListNode pre=dummyNode;
            //1.确认第一段的结束位置,从虚拟头节点走left-1步,来到left结点前一个结点
            for(int i=0;i<left-1;i++){
                pre=pre.next;
            }
            ListNode rightNode=pre;
            //2.确认反转链表区间的最右边的结点,从pre再走right-left+1步,来到right结点
            for(int i=0;i<right-left+1;i++){
                rightNode=rightNode.next;
            }
            //3.切出一个子链表
            //反转链表区间最左边的结点
            ListNode leftNode=pre.next;
            //最后一段结点
            ListNode succ=rightNode.next;
            //这里设置next为null,是为了切出反转链表的区间
            rightNode.next=null;
            //4.反转链表的子区间
            reverseLinkedList(leftNode);
            //5.将链表穿插接,因为区间链表已经反转了,所以让pre.next指向rightNode
            //leftNode.next指向最后一段,就将三段链表接在一块了
            pre.next=rightNode;
            leftNode.next=succ;
            return dummyNode.next;
    }

    public ListNode reverseLinkedList(ListNode head){
        //不使用虚拟结点进行反转
        ListNode cur=head;
        ListNode pre=null;
        while(cur!=null){
            ListNode next=cur.next;
            cur.next=pre;
            pre=cur;
            cur=next;
        }
        return head;
    }

方法二的缺点是: 如果left 和 right 的区域很大,恰好是链表的头节点和尾节点时,找到left 和 right需要遍历一次,反转它们之间的链表还需要遍历一次,虽然总的时间复杂度为 O(N),历了链表 2次,而方法一只需要遍历链表一次

2.两两交换链表中的结点(LeetCode24)

算法通关村第二关[白银挑战]-链表反转_第8张图片

如果原始顺序是 dummy -> node1 -> node2,交换后面两个节点关系要变成 dummy -> node2->node1,事实上我们只要多执行一次next就可以拿到后面的元素,也就是类似node2 = temp.next.next这样的操作。 两两交换链表中的节点之后,新的链表的头节点是 dummyHead.next,返回新的链表的头节点即可。指针的调整可以参考如下图示:

算法通关村第二关[白银挑战]-链表反转_第9张图片

    /**
     * 
     * @param head
     * @return
     */
    public ListNode swapPairs(ListNode head) {
        ListNode dummyNode=new ListNode(-1);
        dummyNode.next=head;
        ListNode temp=dummyNode;
        while(temp.next!=null&&temp.next.next!=null){
            ListNode node1=temp.next;
            ListNode node2=temp.next.next;
            //temp指向下下一个结点
            temp.next=node2;
            //然后让node1指向node2的下一个结点
            node1.next=node2.next;
            //然后再让node2指向node1,达成两两互换
            node2.next=node1;
            //同时让temp排在下一批要互换的结点前,就是此时node1
            temp=node1;
        }
        return dummyNode.next;
    }

3.单链表+1(LeetCode369)

用一个非空单链表来表示一个非负整数,然后将这个整数加一。你可以假设这个整数除了0本身,没有任何前导的 0。这个整数的各人数位按照 高位在链表头部、低位在链表尾部的顺序排列。
示例:输入: [1,2,3]输出: [1,2,4]
算法通关村第二关[白银挑战]-链表反转_第10张图片
加法的计算过程:
计算是从低位开始的,而链表是从高位开始的,所以要处理就必须反转过来,此时可以使用栈,也可以使用链表反转来实现。
基于栈实现的思路不算复杂,先把题目给出的链表遍历放到栈中,然后从栈中弹出栈顶数字 digit,加的时候再考虑一下进位的情况就ok了,加完之后根据是否大于0决定视为下一次要进位。

3.1 使用栈进行反转

/**  方法一:使用栈
     * @param head
     * @return {@link ListNode}
     */
    public static ListNode plusOne(ListNode head) {
        Stack<Integer> stack = new Stack<>();
        //链表元素压栈
        while (head != null) {
            stack.push(head.val);
            head = head.next;
        }
        //是否要进1
        int carry = 0;
        //建立一个新链表,用于拼接
        ListNode dummy = new ListNode(0);
        //添加的数,需要以变量方式,当低位加完数后就不再加
        int addr = 1;
        //栈空时结束或当前元素是最高位,并满10需要进1,此时栈空,carry就会>1
        while (!stack.empty() || carry > 0) {
            int digit = stack.empty() ? 0 : stack.pop();
            //值+需要加的数+进1数
            int sum = digit + addr + carry;
            //判断是否要进1
            carry = sum >= 10 ? 1 : 0;
            //判断数是否大于10,大于10需减去10进一
            sum = sum >= 10 ? sum - 10 : sum;
            //添加结点到开头,使得链表反转回来
            ListNode newNode = new ListNode(sum);
            newNode.next = dummy.next;
            dummy.next = newNode;
            //加完数归0
            addr = 0;
        }
        return dummy.next;
    }

3.2 使用链表进行反转

    /** 方法二:通过链表反转,再反转回去
     * @param head
     * @return {@link ListNode}
     */
    public static ListNode plusOne1(ListNode head) {
        ListNode reverse = reverseLinkedList(head);
        ListNode temp=reverse;
        int carry=0;
        int addr=1;
        while (reverse!=null){
            int val = reverse.val;
            int sum=val+addr+carry;
            carry=sum>=10?1:0;
            sum=sum>=10?sum-10:sum;
            //当下一个结点不存在并且需要进1时
            if (reverse.next==null&&carry>0){
                ListNode newNode = new ListNode(sum);
                reverse.next=newNode;
            }
            reverse.val=sum;
            reverse=reverse.next;
            addr=0;
        }
        return reverseLinkedList(temp);
    }

    /**不利用头结点链表反转
     * @param head
     * @return {@link ListNode}
     */
    public static ListNode reverseLinkedList(ListNode head){
        if (head==null){
            return null;
        }
        ListNode pre=null;
        ListNode cur=head;
        while (cur!=null){
            ListNode next=cur.next;
            cur.next=pre;
            pre=cur;
            cur=next;
        }
        return pre;
    }

4.链表加法(LeetCode445)

算法通关村第二关[白银挑战]-链表反转_第11张图片

4.1 用栈实现

思路:先将两个链表的元素分别压栈,然后再一起出栈,将两个结果分别计算。之后对计算结果取模,模数保存到新的链表中,进位保存到下一轮。完成之后再进行一次反转就行了。

在链表插入有头插法和尾插法两种。头插法就是每次都将新的结点插入到head之前。而尾插法就是将新结点都插入到链表的表尾。两者的区别是尾插法的顺序与原始链表是一致的,而头插法与原始链表是逆序的,所以上面最后一步如果不想进行反转,可以将新结点以头插法

    /**
     * 通过栈来实现
     * @param head1
     * @param head2
     * @return
     */
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        Stack<Integer> stack1=new Stack();
        Stack<Integer> stack2=new Stack();
        //两个链表压栈
        while(l1!=null){
            stack1.push(l1.val);
            l1=l1.next;
        }
        while(l2!=null){
            stack2.push(l2.val);
            l2=l2.next;
        }
        int carry=0;
        ListNode dummy=new ListNode(0);
        //判断是否栈空和是否要进1
        while(!stack1.empty()||!stack2.empty()||carry>0){
            int sum=carry;
            //栈不为空时取出元素求和
            if(!stack1.empty()){
                 sum+=stack1.pop();
            }
            if(!stack2.empty()){
                 sum+=stack2.pop();
            }
            carry=sum>=10?1:0;
            sum=sum>=10?sum-10:sum;
            ListNode newNode=new ListNode(sum);
            newNode.next=dummy.next;
            dummy.next=newNode;

        }
        return dummy.next;
    }

4.2 使用链表反转实现

如果使用链表反转,先将两个链表分别反转,最后计算完之后再将结果反转,一共有三次反转操作,所以必然将反转抽取出一个方法比较好

    /** 反转链表
     * @param l1
     * @param l2
     * @return {@link ListNode}
     */
    public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        l1=reverse(l1);
        l2=reverse(l2);
        ListNode head=new ListNode(0);
        ListNode cur=head;
        int carry=0;
        while(l1!=null||l2!=null||carry>0){
            int sum=carry;
            if(l1!=null){
                sum+=l1.val;
                l1=l1.next;
            }
            if(l2!=null){
                sum+=l2.val;
                l2=l2.next;
            }
            carry=sum/10;
            sum=sum%10;
            ListNode newNode=new ListNode(sum);
            cur.next=newNode;
        }
        return reverse(head);
    }

    /** 反转链表
     * @param head
     * @return {@link ListNode}
     */
    public static ListNode reverse(ListNode head){
        ListNode cur=head;
        ListNode pre=null;
        while(cur!=null){
            ListNode next=cur.next;
            cur.next=pre;
            pre=cur;
            cur=next;
        }
        return pre;
    }

5.再论链表回文序列的问题

链表回文串的时候,介绍的是基于栈的,相对来说比较好理解,但是除此之外还有可以使用链表反转来进行,而且还可以只反转一半链表,这种方式节省空间。姑且称之为“快慢指针+一半反转”法。
这个实现方式的难度,主要是在while循环中pre.next = prepre和prepre = pre两行实现了一边遍历一边将访问过的链表给反转了

    /**
     * 通过快慢指针+链表反转的方式来判断
     *
     * @param head
     * @return
     */
    public static boolean isPalindromeByTwoPoints(ListNode head) {
        if (head == null || head.next == null) {
            return true;
        }
        ListNode slow = head, fast = head;
        ListNode pre = head, prepre = null;
        //pre是反转存储了前半段倒置的链表
        while (fast != null && fast.next != null) {
            //存储slow遍历的每个结点,直到中间
            pre = slow;
            slow = slow.next;
            fast = fast.next.next;
            //指向前一个结点
            pre.next = prepre;
            //存储前一个结点
            prepre = pre;
        }
        //如果奇数情况,需要跳过中间元素
        if (fast != null) {
            slow = slow.next;
        }
        //遍历判断值是否相同
        while (pre != null && slow != null) {
            if (pre.val != slow.val) {
                return false;
            }
            pre = pre.next;
            slow = slow.next;
        }
        return true;
    }

你可能感兴趣的:(算法通关村,算法,链表,数据结构)