OptimalSolution(3)--链表问题(1)简单

  单链表Node节点类

public class Node {

    public int val;
    public Node next;

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

  双链表DoubleNode类

public class DoubleNode {

    public int val;
    public DoubleNode last;
    public DoubleNode next;
    
    public DoubleNode(int val) {
        this.val = val;
    }
}

  

  一、打印两个有序链表的公共部分

  问题:给定两个有序链表head1和head2,打印两个链表的公共部分

  解答:(1)如果head1的值小于head2,则head1向下移动。(2)如果head2的值小于head1的值,则head2向下移动。(3)如果head1的值等于head2的值,则打印这个值,然后head1和head2都往下移动。(4)head1或head2有任意一个移动到null,则整个过程停止

    public void printCommonPart(Node head1, Node head2) {
        while (head1 != null && head2 != null) {
            if (head1.val < head2.val) {
                head1 = head1.next;
            } else if (head1.val > head2.val) {
                head2 = head2.next;
            } else {
                System.out.print(head1.val + " ");
                head1 = head1.next;
                head2 = head2.next;
            }
        }
        System.out.println();
    }
打印两个有序链表的公共部分

  

  二、在单链表和双链表中删除倒数第K个节点

  题目1:删除单链表中倒数第K个节点

  在删除问题中,如果想要删除某一个节点,就必须定位到要删除节点的前一个节点。

  解答:

  (1)如果链表为空或者K值小于1,则直接返回即可。否则让链表从头开始走到尾,每移动一步,就让K值减1,链表走到结尾时,判断K值的大小

  (2)如果K值大于0(1→2→3,K=4,K:3 2 1),说明链表没有倒数第K个节点,直接返回即可。

  (3)如果K值等于0(1→2→3,K=3,K:2 1 0),说明倒数第K个节点就是头节点,直接删除头节点,返回head.next即可

  (4)如果K值小于0(1→2→3,K=2,K:1 0 -1),此时,重新从头节点开始走,每移动一步,就让K的值加1,当K等于0时,移动停止,此时的节点就是要删除节点的前一个节点。(假设链表长度为N,那么倒数第K个节点的前一个节点就是第N-K个节点,第一次遍历后,K变为K-N,第二次遍历后,遍历到第N-K个节点时K为0)

    public Node removeLastKthNode(Node head, int lastKth) {
        if (head == null || lastKth < 1) {
            return head;
        }
        Node cur = head;
        while (cur != null) {
            lastKth--;
            cur = cur.next;
        }
        if (lastKth == 0) {
            head = head.next;
        }
        if (lastKth < 0) {
            cur = head;
            while (++lastKth != 0) {
                cur = cur.next;
            }
            cur.next = cur.next.next;
        }
        return head;
    }
删除单链表中倒数第K个节点

  题目2:删除双链表中倒数第K个节点(和单链表同理,只是注意last指针的重连即可)

    public DoubleNode removeLastKthNode(DoubleNode head, int lastKth) {
        if (head == null || lastKth < 1) {
            return head;
        }
        DoubleNode cur = head;
        while (cur != null) {
            lastKth--;
            cur = cur.next;
        }
        if (lastKth == 0) {
            head = head.next;
            head.last = null;
        }
        if (lastKth < 0) {
            cur = head;
            while (++lastKth != 0) {
                cur = cur.next;
            }
            DoubleNode newNext = cur.next.next;
            cur.next = newNext;
            if (newNext != null) {
                newNext.last = cur;
            }
        }
        return head;
    }
删除双链表中倒数第K个节点

  

  三、删除链表的中间节点和a/b处的节点

  题目1:删除链表的中间节点,例如1→2删除节点1,1→2→3删除节点2,1→2→3→4删除节点2,1→2→3→4→5删除节点3

  解答:如果链表为空或长度为1,则直接返回,如果链表长度为2,删除头节点即可,则3-2,4-2,5-3,6-3...,也就是长度每增加2,要删除的节点就后移一个节点。

    public Node removeMidNode(Node head) {
        if (head == null || head.next == null) {
            return head;
        }
        if (head.next.next == null) {
            return head.next;
        }
        Node pre = head;
        Node cur = head.next.next;
        while (cur.next != null && cur.next.next != null) {
            pre = pre.next;
            cur = cur.next.next;
        }
        pre.next = pre.next.next;
        return head;
    }
删除链表的中间节点

  题目2:删除位于a/b处节点,链表1→2→3→4→5,假设a/b=r,则r==0不删除任何节点,r在(0,1/5]上删除节点1,r在(1/5,2/5]删除节点2,如果r大于1,不删除任何节点

  解答:根据链表长度n,以及a和b先计算出n*(a/b)的值,然后r向上取整就是要删除的节点的位置。

    public Node removeByRatio(Node head, int a, int b) {
        if (a < 1 || a > b) {
            return head;
        }
        int n = 0;
        Node cur = head;
        while (cur != null) {
            n++;
            cur = cur.next;
        }
        n = (int)Math.ceil((double) (a * n) / (double)b);
        if (n == 1) {
            head = head.next;
        }
        if (n > 1) {
            cur = head;
            while (--n != 1) {
                cur = cur.next;
            }
            cur.next = cur.next.next;
        }
        return head;
    }
删除单链表中a/b处的节点

 

  四、反转单向和双向链表

  题目1:反转单向链表

    public Node reverseList(Node head) {
        Node pre  = null;
        Node next = null;
        while (head != null) {
            next = head.next;
            head.next = pre;
            pre = head;
            head = next;
        }
        return pre;
    }
反转单链表

  题目2:反转双向链表

    public DoubleNode reverseList(DoubleNode head) {
        DoubleNode pre  = null;
        DoubleNode next = null;
        while (head != null) {
            next = head.next;
            head.next = pre;
            head.last = next;
            pre = head;
            head = next;
        }
        return pre;
    }
反转双向链表

 

  五、反转部分单向链表

  题目:给定一个单链表和两个整数from和to,把第from个到第to个节点这一部分进行反转。例如1→2→3→4→5→null,from=2,to=4,则1→4→3→2→5→null

  解答:

  (1)如果1<=from<=to<=N不满足,返回原来的头节点

  (2)找到第from-1个节点fPre和第to+1个节点tPos,把反转的部分先反转,然后正确连接fPre和tPos

  (3)如果fPre为null,说明反转部分是包含头节点的,返回每反转以前反转部分的最后一个节点作为新的头节点,如果fPre不为null,返回旧的头节点。

    public Node reversePart(Node head, int from, int to) {
        int len = 0;
        Node node1 = head;
        Node fPre = null;
        Node tPos = null;
        while (node1 != null) {
            len++;
            fPre = len == from - 1 ? node1 : fPre;
            tPos = len == to +   1 ? node1 : tPos;
            node1 = node1.next;
        }
        if (from > to || from < 1 || to > len) {
            return head;
        }
        node1 = fPre == null ? head : fPre.next;
        Node node2 = node1.next;
        node1.next = tPos;
        Node next = null;
        while (node2 != tPos) {
            next = node2.next;
            node2.next = node1;
            node1 = node2;
            node2 = next;
        }
        if (fPre != null) {
            fPre.next = node1;
            return head;
        }
        return node1;
    }
反转部分单向链表

 

  六、两个单链表生成相加链表

  问题:9→3→7代表整数937,6→3代表整数63,将两个链表相加得到937+63=1000,返回1→0→0→0

  思路:如果先分别算出各自链表所代表的整数,那么链表的长度很长,可以表示一个很大的数,转换成int时可能会异常

  解法1:利用栈结构

  (1)将两个链表从左到右遍历,并压入各自的栈中。因此s1:9 3 7,s2:6 3,

  (2)将s1和s2同步弹出,然后相加链表即可,同时还需要关注是否有进位  

  (3)当s1和s2都为空时,如果进位信息为1,还要生成一个节点值为1的新结点

   注意:两个整数相加与进位问题。1. n1 = s1.isEmpty() ? 0 : s1.pop();2.n = n1 + n2 + ca;3.node = new Node(n % 10);4.ca = n / 10;

   注意:从后往前生成链表的过程。 pre = node; node = new Node(n % 10); node.next = pre;

    public Node addLists1(Node head1, Node head2) {
        Stack<Integer> s1 = new Stack<>();
        Stack<Integer> s2 = new Stack<>();
        while (head1 != null) {
            s1.push(head1.val);
            head1 = head1.next;
        }
        while (head2 != null) {
            s2.push(head2.val);
            head2 = head2.next;
        }
        int ca = 0;
        int n1 = 0, n2 = 0;
        int n = 0;
        Node node = null;
        Node pre  = null;
        while (!s1.isEmpty() || !s2.isEmpty()) {
            n1 = s1.isEmpty() ? 0 : s1.pop();
            n2 = s2.isEmpty() ? 0 : s2.pop();
            n = n1 + n2 + ca;
            pre = node;
            node = new Node(n % 10);
            node.next = pre;
            ca = n / 10;
        }
        if (ca == 1) {
            pre = node;
            node = new Node(1);
            node.next = pre;
        }
        return node;
    }

  解法2:利用链表的逆序

  (1)将两个链表逆序,依次得到从低位到高位的数字

  (2)遍历两个逆序后的链表,过程和解法1类似

  需要注意:1.1 = c1 != null ? c1.val : 0; 2.c1 = c1 != null ? c1.next : null; 3.完成后还需要reverseList(head1); reverseList(head2);将原来的链表返回原状。

    public Node addList2(Node head1, Node head2) {
        head1 = reverseList(head1);
        head2 = reverseList(head2);
        int ca = 0, n1 = 0, n2 = 0, n = 0;
        Node c1 = head1;
        Node c2 = head2;
        Node node = null;
        Node pre  = null;
        while (c1 != null || c2 != null) {
            n1 = c1 != null ? c1.val : 0;
            n2 = c2 != null ? c2.val : 0;
            n = n1 + n2 + ca;
            pre = node;
            node = new Node(n%10);
            node.next = pre;
            ca = n / 10;
            c1 = c1 != null ? c1.next : null;
            c2 = c2 != null ? c2.next : null;
        }
        if (ca == 1) {
            pre = node;
            node = new Node(1);
            node.next = pre;
        }
        reverseList(head1);
        reverseList(head2);
        return node;
    }
利用链表的逆序

 

  七、删除无序单链表中值重复出现的节点

  问题:给定一个无序单链表,删除其中值重复出现的节点,例如1→2→3→3→4→4→2→1→1→null,返回1→2→3→4→null。

  解法1:利用哈希表,但是时间复杂度为O(N),空间复杂度为O(N)

  最近一个没有被删除的节点为pre,当前节点cur的值如果出现过,就令pre.next=cur.next;如果没出现过,就将cur的值加入到哈希表,同时令pre=cur。

    public void removeRep1(Node head) {
        if (head == null) {
            return;
        }
        HashSet<Integer> set = new HashSet<>();
        Node pre = head;
        Node cur = head.next;
        set.add(head.val);
        while (cur != null) {
            if (set.contains(cur.val)) {
                pre.next = cur.next;
            } else {
                set.add(cur.val);
                pre = cur;
            }
            cur = cur.next;
        }
    }
删除无序单链表中重复节点

  解法2:利用选择排序的过程,时间复杂度为O(N2),空间复杂度为O(1)(直接选择排序时间复杂度为O(N2))

  例如1→2→3→3→4→4→2→1→1→null,(1)cur值为1,检查后面所有值为1的节点全部删除(2)cur值为2,检查后面值为2的节点全部删除...依次类推

    public void removeRep2(Node head) {
        Node cur = head;
        Node pre = null;
        Node next = null;
        while(cur != null) {
            pre = cur;
            next = cur.next;
            while (next != null) {
                if (cur.val == next.val) {
                    pre.next = next.next;
                } else {
                    pre = next;
                }
                next = next.next;
            }
            cur = cur.next;
        }
直接选择排序思想删除重复节点

 

  八、在单链表删除指定值的节点

  题目:给定一个单链表和一个整数num,删除值为num的全部节点。例如:1→2→3→4→null,num=3,返回:1→2→4→null

  方法1:O(N)+O(N)利用栈或者其他容器收集值不等于num的节点,收集完成后重新连接即可。最后返回栈底节点为新的头结点

    public Node removeValue1(Node head, int num) {
        Stack<Node> stack = new Stack<>();
        while(head != null) {
            if (head.val != num) {
                stack.push(head);
            }
            head = head.next;
        }
        while (!stack.isEmpty()) {
            stack.peek().next = head;    // head一开始为null
            head = stack.pop();
        }
        return head;
    }
利用栈删除单链表中指定值

  方法2:O(N)+O(1)不用任何容器直接调整。找到第一个不为num的节点作为新的头结点newHead,继续往后遍历,如果cur的值等于num,就将cur删除,即令最近一个值不等于num的pre.next=cur.next,如果不等于,就令pre=cur

    public Node removeValue2(Node head, int num) {
        while (head != null) {
            if (head.val != num) {
                break;
            }
            head = head.next;
        }
        Node pre = head;
        Node cur = head;
        while(cur != null) {
            if (cur.val == num) {
                pre.next = cur.next;
            } else {
                pre = cur;
            }
            cur = cur.next;
        }
        return head;
    }
直接调整法删除单链表中指定值

  

  九、单链表的选择排序

  题目:给定一个无序单链表,实现单链表的选择排序,要求空间复杂度为O(1)

  选择排序就是:从未排序的部分中找到最小值,然后放在排好序部分的尾部,逐渐将未排序的部分缩小,最后全部变成排好序的部分。

  解法:O(N2)+O(1)

  (1)找到最小值节点,将其设置成新的头节点newHead

  (2)每次在未排序的部分中找到最小值的节点,然后把这个节点从未排序的链表中删除,同时连接删除节点的前后节点

  (3)把删除的节点连接在排好序部分的链表尾部

    public Node getSmallestPreNode(Node head) {
        Node smallPre = null;
        Node small = head;
        Node pre = head;
        Node cur = head.next;
        while (cur != null) {
            if (cur.val < small.val) {
                smallPre = pre;
                small = cur;
            }
            pre = cur;
            cur = cur.next;
        }
        return smallPre;// 返回以head为头结点的单链表中最小值节点的前一个节点
    }
    
    public Node selectionSort(Node head) {
        Node tail = null;        // 已排序部分尾部
        Node cur  = head;        // 未排序部分头部
        Node smallPre = null;    // 最小节点的前一个节点
        Node small = null;        // 值最小的节点
        while (cur != null) {
            small = cur;
            smallPre = getSmallestPreNode(cur);
            if (smallPre != null) {
                // small为未排序部分的最小值
                small = smallPre.next;
                // 将最小值节点从未排序的部分删除
                smallPre.next = small.next;
            }
            // smallPre为null时,说明当前节点cur已经是最小值,因此在原链表中删除头节点
            cur = cur == small ? cur.next : cur;
            if (tail == null) {
                head = small;
            } else {
                tail.next = small;
            }
            tail = small;
        }
        return head;
    }

 

  十、一种怪异的节点删除方式

  问题:给定一个链表的节点node,但不给定整个整个链表的头节点,如何在链表中删除node?并分析出现的问题

  思路:例如1→2→3→null,如果要删除节点2,那么在仅给定节点2的情况下,将节点2的值变成3,然后删除结点3即可。(在链表中删除节点的原则,必须定位到待删节点的上一个节点)

  可能出现的问题:

  (1)无法删除最后一个节点。因为如果要删除节点3,由于根本没有下一个节点来代替节点3被删除,那么只有让节点2的next指向null这一种办法,但是又根本找不到节点2,所以就无法正确删除节点3。

  (2)这种删除方式在本质上根本不是删除了node节点,而是把node节点的值改变,然后删除node的下一个节点。在实际工程中,一个节点可能代表很复杂的结构,节点的复制会相当复杂,或者可能改变节点值这个操作都是被禁止的;或者工程上一节点代表提供服务的一个服务器,外界对每个节点都有很多依赖,例如,在删除节点2时,实际上影响了节点3对外提供的服务。

  注意:异常的抛出,RuntimeException异常类不需要导包。

    public void removeNodeWired(Node node) {
        if (node == null) {
            return;
        }
        Node next = node.next;
        if (next == null) {
            throw new RuntimeException("can not remove last node.");
        }
        node.val = next.val;
        node.next = next.next;
    }

 

  十一、向有序的环形单链表中插入新节点

  问题:一个环形单链表从头节点head开始不降序,同时由最后的节点指向头节点。给定一个环形单链表的头节点和一个整数num,生成节点值为num的新节点,并插入到这个环形链表中,保证调整后的链表依然有序。

  解法:

  (1)生成节点值为num的新节点,记为node

  (2)如果链表为空,直接返回node

  (3)令pre=head,cur=head.next,令pre和cur同步移动,如果pre的节点值小于或等于num,并且cur的节点值大于或等于num,说明node应该在pre和cur节点之间插入

  (4)如果pre和cur转了一圈都没有发现插入位置,说明node应该插入到头节点的前面,此时是因为链表中每个节点都大于num或者都小于num。如果num比每个节点值都大,返回原来的头节点即可;如果num比每个节点的值都小,应该把node作为链表新的头节点返回。

    public Node insertNum(Node head, int num) {
        Node node = new Node(num);
        if (head == null) {
            node.next = node;
            return node;
        }
        Node pre = head;
        Node cur = head.next;
        while (cur != head) {
            if (pre.val <= num && cur.val >= num) {
                break;
            }
            pre = cur;
            cur = cur.next;
        }
        pre.next = node;
        node.next = cur;
        return head.val < num ? head : node;
    }
向有序的环形单链表中插入节点

 

  

  十二、合并两个有序的单链表

  题目:给定两个有序单链表head1和head2,返回合并后的链表。例如:0→2→3→7→null,1→3→5→7→9→null,返回0→1→2→3→3→5→7→7→9→null

  解法:(O(M+N) + O(1))

  (1)如果两个链表有一个为空,返回非空链表头节点

  (2)head1和head2中小的节点就是新链表的头节点

  (3)每次比较遍历到的值,根据大小关系作出调整,同时用一个变量pre表示上次比较时值较小的节点

链表1:1→5→6→null, 链表2:2→3→7→null
cur1=1,cur2=2,pre=null,cur1小于cur2,不作调整,pre=cur1
cur1=5,cur2=2,pre=1,cur2小于cur1,pre.next指向cur2,cur2.next指向cur1,pre=2,此时1→2→5→6→null,3→7→null,
cur1=5,cur2=3,pre=2,cur2小于cur1,pre.next指向cur2,cur2.next指向cur1,pre=3,此时1→2→3→5→6→null,7→null
cur1=5,cur2=7,pre=3,cur1小于cur2,不作调整,pre=5
cur1=6,cur2=7,pre=5,cur1小于cur2,不作调整,pre=6,
此时cur1=null,pre为链表1的最后一个节点,把pre.next指向cur2

  (4)如果链表1先走完,此时cur1=null,pre为链表1的最后一个节点,那么就把pre的next指针指向链表2的当前节点,即cur2;如果链表2先走完,说明链表2的所有节点已经插入到链表1中,此时返回新的头节点即可。

  注意:pre这个变量在这道题里面所起到的作用。 pre.next = cur1 == null ? cur2 : cur1;

    public Node merge(Node head1, Node head2) {
        if (head1 == null || head2 == null) {
            return head1 != null ? head1 : head2;
        }
        Node head = head1.val < head2.val ? head1 : head2;
        Node cur1 = head == head1 ? head1 : head2;
        Node cur2 = head == head1 ? head2 : head1;
        Node pre = null;
        Node next = null;
        while (cur1 != null && cur2 != null) {
            if (cur1.val <= cur2.val) {
                pre = cur1;
                cur1 = cur1.next;
            } else {
                next = cur2.next;
                pre.next = cur2;
                cur2.next = cur1;
                pre = cur2;
                cur2 = next;
            }
        }
        pre.next = cur1 == null ? cur2 : cur1;
        return head;
    }

 

  十三、按照左右半区的方式重新组合单链表

  问题:给定一个长度为N的单链表,如果N为偶数,那么前N/2个节点为左半区,后N/2个节点为右半区;如果N为奇数,那么前N/2个节点算作左半区,后N/2+1个节点为右半区,将单链表调整成L1→R1→L2→R2..的形式。例如:1→2→3→4→null调整为1→3→2→4→null,1→2→3→4→5→null调整为1→3→2→4→5→null

  解法:O(N)+O(1)

  (1)如果链表为空或长度为1,则直接返回

  (2)遍历一遍找到左半区的最后一个节点,记为mid,这里涉及到之前做过的题,求中间节点的方法:即从长度为2开始,长度每增加2,mid就向后移动一个节点。例如:12mid为1,123mid为1,1234mid为2,12345mid为2,123456mid为3...

  (3)将左右半区分离成2个链表即mid.next=null,头结点分别为原来的head记为left,和mid.next记为right。

  注意:1.mergeLR方法是将right链表的每一个节点依次插入到left的合适位置中。

     2.求链表中间节点mid的方法: Node mid = head; Node right = head.next; while (right.next != null && right.next.next != null) { mid = mid.next; right = right.next.next; }

    public void relocate(Node head) {
        if (head == null || head.next == null) {
            return;
        }
        Node mid = head;
        Node right = head.next;
        while (right.next != null && right.next.next != null) {
            mid = mid.next;
            right = right.next.next;
        }
        right = mid.next;
        mid.next = null;
        mergeLR(head, right);
    }
    
    public void mergeLR(Node left, Node right) {
        Node next = null;
        while (left.next != null) {
            next = right.next;
            right.next = left.next;
            left.next = right;
            left = right.next;
            right = next;
        }
        left.next = right;
    }

 

posted @ 2018-09-14 14:15  BigJunOba  阅读(233)  评论(0编辑  收藏  举报