第四章 链表

21、删除倒数第k个节点

题目:如果给定一个链表,请问如何删除链表中的倒数第k个节点?假设链表中节点的总数为n,那么1≤k≤n。要求只能遍历链表一次。例如,输入下图1中的链表,删除倒数第2个节点之后的链表如下图2所示。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

public class test0101 {

    public static void main(String[] args) {

        int k = 2;
        //创建一个链表 1→2→3→4→5→6
        ListNode head = new ListNode(1);
        ListNode node;
        node = head;
        for (int i = 0; i < 5; i++) {
            node.next = new ListNode(i + 2);
            node = node.next;
        }

        //获取处理后链表的头节点
        ListNode listNode = deleteListNode(head, 2);

        //遍历链表输出
        while (listNode != null){
            System.out.println(listNode.val);
            listNode = listNode.next;
        }

    }

    private static ListNode deleteListNode(ListNode head,int k){

        //新建一个哨兵节点,值val为0,在通过new创建时传入
        ListNode sentinel = new ListNode(0);

        //哨兵节点的next指向head头节点
        sentinel.next = head;
        ListNode node_right = head;
        ListNode node_left = sentinel;

        //让前指针先走k+1步(起始位置就比后指针领先一位),这样当前指针为null时,后指针的下一位就是要找的倒数第k位节点
        for (int i = 0; i < k; i++) {
            node_right = node_right.next;
        }

        //当前指针为null时,表示前指针已经移动到链表最后一位的下一位
        while (node_right != null){
            node_right = node_right.next;
            node_left = node_left.next;
        }

        //assert断言语句,结果为true则正常执行,结果为false则抛出异常,可以省略
        assert node_left.next != null;
        node_left.next = node_left.next.next;

        return sentinel.next;
    }

}

22、链表中环的入口节点

题目:如果一个链表中包含环,那么应该如何找出环的入口节点?从链表的头节点开始顺着next指针方向进入环的第1个节点为环的入口节点。例如,在下图所示的链表中,环的入口节点是节点3。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

解法一:

public class test0201 {

    public static void main(String[] args) {

    }

    private static ListNode findStartNode(ListNode head){

        ListNode circleNode = getCircleNode(head);  //获取循环链表的循环中的一个节点
        if (circleNode == null){
            return null;
        }

        ListNode countNode = circleNode.next;  //获取循环部分的节点数目
        int count = 1;
        while (countNode != circleNode){
            countNode = countNode.next;
            count++;
        }

        ListNode front = head;
        ListNode back = head;
        for (int i = 0; i < count; i++) {  //将前指针前移循环部分节点数目步,后指针位于头节点处
            front = front.next;
        }

        //要使前后两个指针相遇的位置是循环部分的第一个节点,需要保证
        while (front != back){  //前指针与后指针按相同速度向前移动,直到两个指针指向同一个节点,那么该节点就是循环部分的第一个节点
            front = front.next;
            back = back.next;
        }

        return back;
    }

    private static ListNode getCircleNode(ListNode head){

        if (head == null || head.next == null){
            return null;
        }

        ListNode back = head;
        ListNode front = back.next;

        while (back != null && front != null){

            if (back == front){
                return back;
            }

            back = back.next;
            front = front.next;
            if (front != null){
                front = front.next;
            }else {
                return null;
            }

        }

        return null;
    }

}

解法二:

public class test0202 {
    
    //方法二:起始并不需要知道循环部分节点数量,使用前后指针按2:1的移动速度找到循环部分的一个节点时
    //找到的节点到循环尾节点数目 + 多个完整循环节点数目 = 头节点到循环第一个节点前一个节点的数目
    //因此可以让前指针位于找到的循环中的节点位置,后指针位于头节点位置,两个指针按相同速度移动,相遇位置就是循环第一个节点
    private static ListNode findStartNode(ListNode head){

        ListNode circleNode = getCircleNode(head);  //获取链表循环部分的一个节点
        if (circleNode == null){
            return null;
        }

        ListNode back = head;  //将后指针指向头节点

        while (circleNode != back){  //后指针与链表循环部分节点指针同时移动,两者相等的节点就是循环部分第一个节点
            circleNode = circleNode.next;
            back = back.next;
        }

        return back;
    }

    private static ListNode getCircleNode(ListNode head){

        if (head == null || head.next == null){
            return null;
        }

        ListNode back = head;
        ListNode front = back.next;

        while (back != null && front != null){

            if (back == front){
                return back;
            }

            back = back.next;
            front = front.next;
            if (front != null){
                front = front.next;
            }else {
                return null;
            }

        }

        return null;
    }

}

23、两个链表的第1个重合节点

题目:输入两个单向链表,请问如何找出它们的第1个重合节点。例如,下图中的两个链表的第1个重合节点的值是4。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

public class test0301 {

    private static ListNode intersectNode(ListNode head1, ListNode head2){

        if (head1 == null || head2 == null){  //当任意链表为空,返回空
            return null;
        }

        int count1 = countNode(head1);  //两个链表分别遍历一遍,得到两个链表的节点数
        int count2 = countNode(head2);

        ListNode less;  //使用less节点指向较短链表的头节点,more节点指向较长链表的头节点
        ListNode more;
        if (count1 == count2){
            less = head1;
            more = head2;
        }else {
            less = count1 > count2 ? head2:head1;
            more = count1 > count2 ? head1:head2;
        }

        int frontStep = count1 >= count2 ? count1-count2:count2-count1;

        for (int i = 0; i < frontStep; i++) {  //将较长链表的more指针移动两链表长度之差步数,保证两链表剩余长度相等
            more = more.next;
        }

        while (more != less){
            more = more.next;
            less = less.next;
        }

        return more;
    }

    private static int countNode(ListNode head){  //遍历链表,获取链表节点数

        int count = 0;
        ListNode node = head;

        while (node != null){
            count++;
            node = node.next;
        }

        return count;
    }

}

24、反转链表

题目:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。例如,把下图1中的链表反转之后得到的链表如图2所示。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

public class test0401 {

    private static ListNode reverseLinked(ListNode head){

        //前指针pre要初始化为null,因为翻转后pre是链表最后一个节点
        ListNode pre = null;  //先初始化定义前中后三个指针,pre,cur,after,cur指针指向当前要更改next指针的链表节点
        if (head == null){
            return null;
        }
        ListNode cur = head;
        ListNode after = head.next;

//        while (cur != null){
//            //一定要在每次循环开始的时候将后指针after移动到cur的next指针指向的节点,如果在每次循环结束时移动after指针,则最后一个节点cur为null时则无法移动after指针
//            ListNode after = cur.next;  //将after移动到cur.next的位置
//            cur.next = pre;  //cur节点的next指针指向前一个节点
//            //一定要先移动前指针pre,再移动中指针cur,最后移动后指针after,为了避免after指针在cur移动至null时报错,将最后移动后指针after放到每次循环最开始
//            pre = cur;  //将前指针pre后移一位,移动到cur
//            cur = after;  //将中指针cur后移一位,移动到after
//        }
//        return pre;  //最终cur节点为null跳出循环,pre节点为链表的初始尾节点,也就是翻转之后的第一个节点
        while (after != null){
            cur.next = pre;
            pre = cur;
            cur = after;
            after = after.next;
        }

        return cur;
    }

}

25、链表中的数字相加

题目:给定两个表示非负整数的单向链表,请问如何实现这两个整数的相加并且把它们的和仍然用单向链表表示?链表中的每个节点表示整数十进制的一位,并且头节点对应整数的最高位数而尾节点对应整数的个位数。例如,在图1和图2中,两个链表分别表示整数123和531,它们的和为654,对应的链表如图3所示。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

public class test0501 {

    private static ListNode addDoubleLinked(ListNode head1, ListNode head2){

        ListNode reverse1 = reverseLinked(head1);  //将输入的两个链表翻转
        ListNode reverse2 = reverseLinked(head2);

        ListNode newNode = new ListNode(0);  //新建一个链表,保存求和计算的结果
        int carry = 0;
        int add1 = 0;
        int add2 = 0;
        int sum = 0;

        while (reverse1 != null || reverse2 != null){  //循环遍历两个链表,对每个节点的值求和放入新链表的节点中

            add1 = reverse1 == null?0:reverse1.val;
            add2 = reverse2 == null?0:reverse2.val;

            sum = add1 + add2 + carry>=10?add1+add2-10:add1+add2;
            carry = add1 + add2>=10?1:0;

            newNode.next = new ListNode(sum);
            newNode = newNode.next;

            reverse1 = reverse1 == null?null:reverse1.next;
            reverse2 = reverse2 == null?null:reverse2.next;

        }
        if (carry != 0){  //当最后进位不为0时,在结果链表中再增加一个节点保存最后进位信息
            newNode.next = new ListNode(carry);
            newNode = newNode.next;
        }

        return reverseLinked(newNode);  //将结果链表翻转就是最终的结果链表
    }

    private static ListNode reverseLinked(ListNode head){

        ListNode pre = null;
        ListNode cur = head;

        while (cur != null){
            ListNode after = cur.next;
            cur.next = pre;  //要把pre赋值给cur的next指针,来让next指针指向pre,而不是把cur的next指针赋值给pre,如果这样,是将pre移动到cur的next指针指向的节点
            pre = cur;
            cur = after;
        }

        return cur;
    }

}

26、重排链表

题目:给定一个链表,链表中节点的顺序是L0→L1→L2→...→Ln-1→Ln,请问如何重排链表使节点的顺序变成L0→Ln→L1→Ln-1→L2→Ln-2→...?例如,输入图1中的链表,重排之后的链表如图2所示。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

import java.util.ArrayList;

public class test0601 {

    public static void main(String[] args) {

        ListNode head = new ListNode(0);
        ListNode res = resultList(reorderedList(head));

    }

    private static ArrayList<ListNode> reorderedList(ListNode head){

        ListNode pre = new ListNode(0);  //新建一个哨兵节点
        pre.next = head;  //pre节点的next指针指向head节点
        ListNode front = pre;
        ListNode back = pre;

        while (front != null && front.next != null){  //控制前链表长度 >= 后链表长度

            front = front.next;  //前后两指针同时后移一位
            back = back.next;
            if (front.next != null){  //前指针如果指向的不是最后一个节点,则前指针后移一位
                front = front.next;
            }

        }

        ListNode start2 = back.next;  //后链表第一个节点为前指针的next指针指向的节点
        ListNode list2 = reverseList(start2);
        front.next = null;  //将前链表的最后一个节点的next指针指向null

        ArrayList<ListNode> listNodes = new ArrayList<>();
        listNodes.add(head);
        listNodes.add(list2);

        return listNodes;
    }

    private static ListNode resultList(ArrayList<ListNode> input){

        ListNode pre = new ListNode(0);  //新建一个哨兵节点指向头节点,也就是前链表的第一个节点
        ListNode node1 = input.get(0);
        pre.next = node1;
        ListNode node2 = input.get(1);

        //将链表2的上一个节点的next指针指向链表1的当前节点,链表1的当前节点的next指针指向链表2的当前节点,pre初始节点可以看作链表2的head节点前的哨兵节点
        while (node1 != null && node2 != null) {

            ListNode temp = node1.next;  //新建一个临时节点,保存前链表当前节点的下一个节点,否则该节点指向后链表当前节点时,下一个节点就会丢失
            pre.next = node1;
            node1.next = node2;

            node1 = temp;  //将前后链表当前节点分别移动至各自链表的下一个节点
            pre = node2;
            node2 = node2.next;
        }

        if (node1 != null){
            pre.next = node1;
        }

        return input.get(0);
    }

    private static ListNode reverseList(ListNode head){  //输入头节点将链表反转

        ListNode pre = new ListNode(0);
        ListNode cur = head;
        pre.next = head;
        ListNode after = head.next;

        while (cur != null){

            after = cur.next;
            cur.next = pre;
            pre = cur;
            cur = after;

        }

        return pre;
    }

}

27、回文链表

题目:如何判断一个链表是不是回文?要求解法的时间复杂度是O(n),并且不得使用超过O(1)的辅助空间。如果一个链表是回文,那么链表的节点序列从前往后看和从后往前看是相同的。例如,图1中的链表的节点序列从前往后看和从后往前看都是1、2、3、3、2、1,因此这是一个回文链表。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

public class test0701 {

    private static boolean isPalindrome(ListNode head){

        ListNode pre = new ListNode(0);
        ListNode front = pre;
        ListNode back = pre;
        ListNode list2;

        if (back.next == null || front.next.next == null){
            return true;
        }

        while (back.next != null && front.next.next != null){

            back = back.next;
            front = front.next.next;

        }

        if (front.next != null){
            list2 = back.next.next;
        }else {
            list2 = back.next;
        }
        back.next = null;

        ListNode list1 = reverseList(head);

        return compareLists(list1,list2);
    }

    private static boolean compareLists(ListNode list1,ListNode list2){

        while (list1 != null && list2 != null){

            if (list1.val != list2.val){
                return false;
            }else {
                list1 = list1.next;
                list2 = list2.next;
            }

        }

        return true;
    }

    private static ListNode reverseList(ListNode head){

        ListNode back = new ListNode(0);
        back.next = head;
        ListNode cur = head;

        while (cur != null){

            ListNode front = cur.next;
            cur.next = back;
            back = cur;
            cur = front;

        }

        return back;
    }

}

28、展平多级双向链表

题目:在一个多级双向链表中,节点除了有两个指针分别指向前后两个节点,还有一个指针指向它的子链表,并且子链表也是一个双向链表,它的节点也有指向子链表的指针。请将这样的多级双向链表展平成普通的双向链表,即所有节点都没有子链表。例如,图1所示是一个多级双向链表,它展平之后如图2所示。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

public class threeListNode {

    public int val;
    public threeListNode next;
    public threeListNode prev;
    public threeListNode child;

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

}

//===============================================================================

public class test0801 {

    //展开有子节点的双向链表,子节点链表全部位于当前节点之后与下一个节点之间,并以双向链表连接
    private static threeListNode expandList(threeListNode head){

        threeListNode node = head;  //初始化一个三向节点从头结点开始遍历,一个三向尾节点初始值为null,记录每一个子链表的尾节点
        threeListNode tail = null;

        while (node != null){  //从头开始遍历链表,直到最后一个节点

            threeListNode next = node.next;  //初始化一个next节点,来保存当前遍历的节点的下一个节点

            if (node.child != null){

                threeListNode child = node.child;
                threeListNode childTail = expandList(child);  //将当前节点的子节点作为头节点传入expandList()中,获取子链表尾节点

                if (next != null){
                    next.prev = childTail;  //将当前节点下一个节点的前指针指向子链表的最后一个节点,得保证next不为null,才有前指针
                }
                childTail.next = next;  //将子链表最后一个节点的后指针指向下一个节点
                node.next = child;  //将当前节点的后指针指向其子节点
                child.prev = node;  //子节点的前指针指向当前节点
                node.child = null;  //将当前节点的子节点指针指向null

                tail = childTail;  //当前节点有子节点时,如果后一个节点为null,那么尾节点就是子链表的尾节点
            }else {
                tail = node;  //使用tail保存当前节点,最后一个当前节点就是链表的最后一个节点
            }

            node = node.next;  //将当前节点逐次后移
        }

        return tail;  //返回tail保存的当前链表最后一个节点
    }

}

29、排序的循环链表

题目:在一个循环链表中节点的值递增排序,请设计一个算法在该循环链表中插入节点,并保证插入节点之后的循环链表仍然是排序的。例如,图1所示是一个排序的循环链表,插入一个值为4的节点之后的链表如图2所示。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

public class test0901 {

    private static ListNode insertNode(ListNode head,ListNode node){  //头节点是循环递增链表中任意的一个节点

        if (head == null){  //头节点为null或只有头节点一个节点的情况要单独处理,因为其他情况要比较插入的节点的值和相邻两个节点值的大小
            head = node;
            node.next = node;
        }else if (head.next == null){
            head.next = node;
            node.next = head;
        }else {
            insertIntoNode(head,node);
        }

        return head;
    }

    private static void insertIntoNode(ListNode head,ListNode node){

        ListNode next = head.next;  //初始化newNode节点指向头节点,next节点指向头节点的下一个节点
        ListNode newNode = head;
        ListNode biggest = head;

        //当要插入节点的值不在newNode当前节点和next下一个节点的值之间,并且next节点的下一个节点不是头节点时,将newNode指针和next指针同步循环后移
        while (!(newNode.val >= node.val && newNode.val <= next.val) && next != head){

            newNode = newNode.next;
            next = next.next;
            if (newNode.val > biggest.val){
                biggest = newNode;
            }

        }
        //循环之后,剩下的情况就是要插入节点的值位于newNode和next节点的值之间,和要插入节点的值大于链表节点最大值或小于链表节点最小值
        if (node.val >= newNode.val && node.val <= next.val){  //当要插入节点的值位于两节点值之间时,将节点插入两节点之间
            newNode.next = node;
            node.next = next;
        }else {  //当要插入节点值大于链表最大值或小于链表最小值时,将节点插入最大值节点与其后一个节点之间
            node.next = biggest.next;
            biggest.next = node;
        }

    }

}

第五章 哈希表

30、插入、删除和随机访问都是O(1)的容器

题目:设计一个数据结构,使如下3个操作的时间复杂度都是O(1)。

  • insert(value):如果数据集中不包含一个数值,则把它添加到数据集中。
  • remove(value):如果数据集中包含一个数值,则把它删除。
  • getRandom():随即返回数据集中的一个数值,要求数据集中每个数字被返回的概率都相同。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;

public class test0101 {

    HashMap<Integer,Integer> numsToLocation;
    ArrayList<Integer> nums;

    public test0101(){
        numsToLocation = new HashMap<>();  //通过构造器初始化HashMap和ArrayList
        nums = new ArrayList<>();
    }

    private boolean insert(Integer val){

        if (numsToLocation.containsKey(val)){  //如果哈希表中已经存在要插入的数值,则无法再次插入,返回false
            return false;
        }
        nums.add(val);  //分别向数组中及哈希表中放入该数值及其在数组中的位置
        numsToLocation.put(val,nums.size()-1);
        return true;
    }

    private boolean remove(Integer val){

        if (!numsToLocation.containsKey(val)){  //如果哈希表中不存在要删除的数值,则无法进行删除
            return false;
        }
        //把数组中最后一个数值移动到要删除的数值的位置,这样只需要删除数组中最后一个数值就可以实现删除目标数值,而且不需要移动数组中其他数值位置
        //在哈希表中将数组最后一个数值的位置更新为要删除的数值的位置
        Integer location = numsToLocation.get(val);
        numsToLocation.put(nums.get(nums.size()-1),location);
        numsToLocation.remove(val);
        nums.set(location,nums.get(nums.size()-1));
        nums.remove(nums.size()-1);
        return true;
    }

    private Integer getRandom(){
        Random random = new Random();
        int location = random.nextInt(nums.size());  //返回 0 到 nums.size()-1 中的任意一个整数
        return nums.get(location);
    }

}

31、最近最少使用缓存

题目:请设计实现一个最近最少使用(Least Recently Used,LRU)缓存,要求如下两个操作的时间复杂度都是O(1)。

  • get(key):如果缓存中存在键key,则返回它对应的值;否则返回-1。
  • put(key,value):如果缓存中之前包含键key,则它的值设为value;否则添加键key及对应的值value。在添加一个键时,如果缓存容量已经满了,则在添加新键之前删除最近最少使用的键(缓存中最长时间没有被使用过的元素)。
import java.util.HashMap;

//LruCache缓存(Least Recently Used),使用哈希表与双向链表来实现get和put,时间复杂度都为 O(1)
//指针可以想象成就是其指向的节点
public class test0201 {

    private HashMap<Integer, ListNode> map;
    private ListNode head;
    private ListNode tail;
    int capacity;

    //通过构造函数来对整个类中要使用的属性进行初始化
    public test0201(int cap){

        this.capacity = cap;  //获取设置缓存大小

        map = new HashMap<>();

        head = new ListNode(-1, -1);  //初始化两个哨兵节点head和tail,方便后续操作
        tail = new ListNode(-1, -1);
        head.next = tail;  //将head和tail两节点的前后指针相连
        tail.prev = head;

    }

    public int get(int key){  //获取目标节点的值

        if (!map.containsKey(key)){  //如果哈希表中没有目标值这个键,则表示链表中没有这个节点,返回-1
            return -1;
        }
        ListNode node = map.get(key);  //获取哈希表中保存的这个节点
        deleteOneNode(node);  //在链表中删除该节点并在链表最后增加该节点,表示是最近使用的
        addNode(node);

        return node.val;  //返回目标节点的值
    }

    public void put(int key,int val){  //向链表中增加一个节点或改变一个节点的值

        if (map.containsKey(key)){  //当哈希表中保存了该节点的键时,获取该节点,在链表中删除该节点并在链表尾增加该节点,之后改变该节点的值为传入的val
            ListNode node = map.get(key);
            deleteOneNode(node);
            addNode(node);
            node.val = val;
        //如果哈希表中没有保存该节点的键,同时哈希表的大小没有超过设置的缓存大小时,在链表尾增加该节点同时在哈希表中增加该节点的键及该节点
        }else if (capacity < map.size()){
            ListNode node = new ListNode(key, val);
            addNode(node);
            map.put(key,node);
        }else {  //如果哈希表中没有保存该节点的键同时哈希表的大小已经达到设置的缓存大小时,删除链表第一个节点,在链表尾增加该节点,并在哈希表中增加该节点的键及该节点
            ListNode node = new ListNode(key, val);
            deleteFirstNode();
            map.remove(head.next.key);
            addNode(node);
            map.put(key,node);
        }

    }

    public void deleteOneNode(ListNode node){  //删除链表中间的某一个目标节点
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    public void addNode(ListNode node){  //在链表最后tail节点之前增加一个目标节点
        tail.prev.next = node;
        node.next = tail;
        node.prev = tail.prev;
        tail.prev = node;
    }

    public void deleteFirstNode(){  //删除最近最少使用的链表head节点next指针指向的的头节点
        head.next = head.next.next;
        head.next.next.prev = head;
    }

}

32、有效的变位词

题目:给定两个字符串s和t,请判断它们是不是一组变位词。在一组变位词中,他们中的字符及每个字符出现的次数都相同,但字符的顺序不能相同。例如,"anagram"和"nagaram"就是一组变位词。

解法一:

public class test0301 {

    private static boolean isAnagram(String s,String t){

        if (s.length() != t.length()){  //如果两个字符串长度不相等,返回false
            return false;
        }
        int[] contains = new int[26];  //初始化一个大小为26的数组,保存26个小写字母个数,下标表示不同字母,值表示不同字母个数

        for (char c : s.toCharArray()) {  //遍历字符串s,每次在数组中每个字符的位置+1
            contains[c-'a']++;
        }

        for (char c : t.toCharArray()) {  //遍历字符串t,当数组中当前字符位置已经是0,则返回false,否则当前字符对应数组位置-1

            if (contains[c-'a']==0){
                return false;
            }
            contains[c-'a']--;

        }

        return true;  //遍历完所有字符串并且没有返回false,则返回true
    }

}

解法二:

import java.util.HashMap;

public class test0302 {

    private static boolean isAnagram(String s,String t){

        if (s.length() != t.length()){  //如果两字符串长度不相等则返回false
            return false;
        }

        HashMap<Character, Integer> map = new HashMap<>();  //新建一个哈希表,键为字符串中的字符,值为每个字符出现的次数

        //遍历字符串s,将字符信息保存到哈希表中,再遍历字符串t,看字符串t中有没有字符没有保存到哈希表中或字符出现次数与哈希表中保存的信息不一致,如果有则返回false
        //遍历完两个字符串未返回false而且两字符串长度又相等,则返回true
        for (char c : s.toCharArray()) {
            map.put(c,map.getOrDefault(c,0)+1);
        }

        for (char c : t.toCharArray()) {
            if (!map.containsKey(c) || map.get(c) == 0){
                return false;
            }
            map.put(c,map.get(c)-1);  //当哈希表中已经保存键c时,put方法就会修改键c对应的值
        }

        return true;
    }

}

33、变位词组

题目:给定一组单词,请将它们按照变位词分组。例如,输入一组单词["eat","tea","tan","ate","nat","bat"],这组单词可以分成3组,分别是["eat","tea","ate"]、["tan","nat"]和["bat"]。假设单词中只包含英文小写字母。

解法一:

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

public class test0401 {

    private static List<List<String>> queryAnagram(List<String> input){

        //新建一个包含26个质数的数组用来对应每个小写字母,则换位词的每个字母乘积相等,而非换位词乘积不相等
        int[] counts = {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101};
        HashMap<Long, List<String>> map = new HashMap<>();

        for (String s : input) {
            long multiply = 1L;
            for (char ch : s.toCharArray()) {
                multiply *= counts[ch-'a'];
            }

            map.putIfAbsent(multiply,new LinkedList<>());  //当哈希表中没有保存该乘积时,放入该乘积为键,新建一个空链表数组为值
            map.get(multiply).add(s);  //直接获取该链表并向链表中添加字符串,即可保存到哈希表的值中

        }

        return new LinkedList<>(map.values());  //新建一个链表数组保存哈希表中所有的值并返回
    }

}

解法二:

import java.util.*;

public class test0402 {

    public static void main(String[] args) {

        HashMap<Object, List<Integer>> map = new HashMap<>();
        map.putIfAbsent(1, new LinkedList<>());  //可以在哈希表的值中直接新建一个链表,直接获取该值通过add()方法添加数值即可实现在哈希表的值中添加数值
        map.get(1).add(2);

        System.out.println(map.get(1));

    }

    private static List<List<String>> queryAnagram(List<String> input){

        HashMap<String, List<String>> map = new HashMap<>();

        for (String s : input) {  //将输入字符串数组中每个字符串转换成字符数组,并进行排序成新字符串,换位词最终排序后的新字符串都相同

            char[] chars = s.toCharArray();
            Arrays.sort(chars);
            String s1 = new String(chars);

            map.putIfAbsent(s1,new LinkedList<>());  //当该排序后的字符串未作为键包存在哈希表中时,向哈希表中存入该键及一个空的链表数组
            map.get(s1).add(s);  //获取该链表数组并放入原字符串s即可向哈希表中存入该同位词
        }

        return new LinkedList<>(map.values());
    }

}

34、外星语言是否排序

题目:有一门外星语言,它的字母表刚好包含所有的英文小写字母,只是字母表的顺序不同。给定一组单词和字母表顺序,请判断这些单词是否按照字母表的顺序排序。例如,输入一组单词["offer","is","coming"],以及字母表顺序"zyxwvutsrqponmlkjihgfedcba",由于字母'o'在字母表中位于'i'的前面,因此单词"offer"排在"is"的前面;同样,由于字母'i'在字母表中位于'c'的前面,因此单词"is"排在"coming"的前面。因此,这一组单词是按照字母表顺序排序的,应该输出true。

import java.util.List;

public class test0501 {

    private static boolean isOrdered(List<String> str,String guide){

        int[] order = new int[guide.length()];
        boolean result = false;

        for (int i = 0; i < guide.length(); i++) {
            order[guide.charAt(i) - 'a'] = i;
        }

        for (int i = 0; i < str.size()-1; i++) {
            String s = str.get(i);
            String t = str.get(i + 1);
            result = judgeOrdered(s, t, order);
        }

        return result;
    }

    private static boolean judgeOrdered(String s,String t,int[] order){

        int i = 0;
        for (;i<s.length() && i<t.length();i++){
            
            if (order[s.charAt(i)-'a'] > order[t.charAt(i)-'a']){
                return false;
            }else if (order[s.charAt(i)-'a'] < order[t.charAt(i)-'a']){
                return true;
            }
            
        }
        
        return i == s.length();
    }

}

35、最小时间差

题目:给定一组范围在00:00至23:59的时间,求任意两个时间之间的最小时间差。例如,输入时间数组["23:50","23:59","00:00"],"23:59"和"00:00"之间只有1分钟的间隔,是最小的时间差。

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

public class test0601 {

    public static void main(String[] args) {

        ArrayList<String> list = new ArrayList<>();
        list.add("00:00");
        list.add("23:50");
        list.add("23:59");

        System.out.println(minGap(list));
    }

    private static int minGap(List<String> times){

        boolean[] timeBooleans = new boolean[1440];  //boolean[] 数组默认初始值为false

        for (String time : times) {  //循环遍历输入的时间数组,将每个时间转换为整数,并将布尔数组对应位置设为true

            String[] split = time.split(":");
            int i = Integer.parseInt(split[0]) * 60 + Integer.parseInt(split[1]);

            if (timeBooleans[i]){
                return 0;
            }
            timeBooleans[i] = true;

        }

        return getMinGap(timeBooleans);
    }

    private static int getMinGap(boolean[] timeBooleans){

        //初始化一个指针prev,依次指向遍历布尔数组时的每个为true的值,以便进行后续操作
        int prev = -1;
        //初始化一个头指针和一个尾指针指向布尔数组的第一个和最后一个为true的值
        int first = timeBooleans.length - 1;
        int last = -1;
        //初始化一个最小差值变量,来保存遍历过程中找到的最小差值
        int minGap = timeBooleans.length - 1;

        for (int i = 0; i < timeBooleans.length; i++) {  //遍历布尔数组,依次计算相邻两个true值之间的差值,找到最小差值

            if (timeBooleans[i]){

                if (prev >= 0){
                    minGap = Math.min(i - prev,minGap);
                }
                prev = i;

                first = Math.min(first,i);
                last = Math.max(last,i);
            }
        }

        //将第一个true值加上1440,也就是第二天的这个时间,计算最后一个true值到该时间的差值与遍历布尔数组得到最小差值中的较小值
        return Math.min(first + timeBooleans.length - last,minGap);
    }

}

第六章 栈

36、后缀表达式

题目:后缀表达式是一种算术表达式,它的操作符在操作数的后面。输入一个用字符串数组表示的后缀表达式,请输出该后缀表达式的计算结果。假设输入的一定是有效的后缀表达式。例如,后缀表达式["2","1","3","*","+"]对应的算术表达式是“2 + 1 * 3”,因此输出它的计算结果5。

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class test0101 {

    public static void main(String[] args) {

        //String[] str = {"2", "1", "3", "*", "+"};  创建字符串数组
        ArrayList<String> list = new ArrayList<>();
        list.add("2");
        list.add("1");
        list.add("3");
        list.add("*");
        list.add("+");

        System.out.println(suffixExpression(list));
    }

    private static int suffixExpression(List<String> input){

        Stack<Integer> stack = new Stack<>();  //新建一个栈,用来保存输入数组遍历过程中的整数
        for (String s : input) {
            switch (s){  //当遍历到的字符串是加减乘除任一种时,从栈中取出最后两个数,按当前字符串的运算规则进行运算,将运算结果压入栈中
                case "+":
                case "-":
                case "*":
                case "/":
                    int num2 = stack.pop();
                    int num1 = stack.pop();
                    stack.push(calculate(num1,num2,s));
                    break;
                default:
                    stack.push(Integer.parseInt(s));  //当遍历到的字符串不是加减乘除时就将字符串转换为整数压入栈中
            }
        }

        return stack.pop();  //最后栈中的唯一的整数就是结果
    }

    private static int calculate(int num1,int num2,String s){

        switch (s){
            case "+":
                return num1 + num2;
            case "-":
                return num1 - num2;
            case "*":
                return num1 * num2;
            case "/":
                return num1 / num2;
            default:
                return 0;
        }
    }
}

37、小行星碰撞

题目:输入一个表示小行星的数组,数组中每个数字的绝对值表示小行星的大小,数字的正负号表示小行星运动的方向,正号表示向右飞行,负号表示向左飞行。如果两颗小行星相撞,那么体积较小的小行星将会爆炸最终消失,体积较大的小行星不受影响。如果相撞的两颗小行星大小相同,那么它们都会爆炸消失。飞行方向相同的小行星永远不会相撞。求最终剩下的小行星。例如,有6颗小行星[4,5,-6,4,8,-5],如图1所示(箭头表示飞行的方向),它们相撞之后最终剩下3颗小行星[-6,4,8]。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

import java.util.Stack;

public class test0201 {

    public static void main(String[] args) {

        int[] input = {4,5,-6,4,8,-5};

        int[] result = starCrash(input);

        for (int i : result) {
            System.out.println(i);
        }

    }

    private static int[] starCrash(int[] input){

        Stack<Integer> stack = new Stack<>();

        //可分为两种情况来考虑,向栈中压入当前遍历到的整数及从栈中弹出栈顶整数
        //从栈中弹出分为两种情况,栈顶为正整数同时小于当前遍历的负整数的绝对值,栈顶为正整数同时等于当前遍历的负整数的绝对值
        //如果小于,则继续循环判断栈中下一个整数是否应该弹出;如果等于,则弹出栈顶整数并且继续遍历下一个数组中的整数
        //栈中压入分为两种情况,栈中为空时,及栈顶元素为负整数时
        for (int i : input) {  //遍历数组,当栈顶数值为正整数小于或等于当前遍历的负整数i的绝对值,则将栈顶元素弹出

            while (!stack.empty() && stack.peek()>0 && i<-stack.peek()){
                stack.pop();
            }
            if (!stack.empty() && -stack.peek()==i && stack.peek()>0){
                stack.pop();
            }else if (stack.empty() || stack.peek()<0 || i>0){
                stack.push(i);
            }

        }

        return stack.stream().mapToInt(i->i).toArray();
    }
}

38、每日温度

题目:输入一个数组,它的每个数字是某天的温度。请计算每天需要等几天才会出现更高的温度。例如,如果输入数组[35,31,33,36,34],那么输出为[3,1,1,0,0]。由于第1天的温度是35℃,要等3天才会出现更高的温度36℃,因此对应的输出为3。第4天的温度是36℃,后面没有更高的温度,它对应的输出是0。其他的以此类推。

import java.util.Stack;

public class test0301 {

    public static void main(String[] args) {

        int[] input = {35,31,33,36,34};
        int[] res = highTemperature(input);

        for (int i : res) {
            System.out.println(i);
        }
    }
    
    private static int[] highTemperature(int[] input){

        Stack<Integer> stack = new Stack<>();  //初始化一个栈,保存数组中数值的下标,栈中元素为递减序列,因为比栈顶小的元素才会入栈
        int[] result = new int[input.length];  //初始化一个结果数组,大小为输入数组大小

        for (int i = 0; i < input.length; i++) {  //循环遍历输入的数组
            //当栈不为空并且栈顶下标对应的数组元素小于当前遍历的数值时,弹出栈顶元素,并将i-pop保存到结果数组中从栈中弹出下标对应的位置
            while (!stack.empty() && input[stack.peek()]<input[i]){
                Integer pop = stack.pop();
                result[pop] = i - pop;  //i-pop为栈顶下标对应的元素到当前遍历的元素的差值,当前遍历的元素就是栈顶元素的之后第一个大于栈顶元素的值
            }
            //当栈不为空并且栈顶元素对应的值大于等于当前遍历的数值,或栈为空时,则将当前遍历到的数值对应的下标压入栈中
            if ((!stack.empty() && input[stack.peek()]>=input[i]) || stack.empty()){  //可去掉该判断,直接向栈中压入i,因为这些条件与while条件相反
                stack.push(i);
            }
        }

        return result;
    }
}

39、直方图最大矩形面积

题目:直方图是由排列在同一基线上的相邻柱子组成的图形。输入一个由非负数组成的数组,数组中的数字是直方图中的柱子的高。求直方图中最大矩形面积。假设直方图中柱子的宽都为1。例如,输入数组[3,2,5,4,6,1,4,2],其对应的直方图如图1所示,该直方图中最大矩形面积为12,如阴影部分所示。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

解法一:

public class test0401 {

    public static void main(String[] args) {

        int[] input = {3,2,5,4,6,1,4,2};
        System.out.println(maxArea(input));

    }

    private static int maxArea(int[] input){

        int res = 0;
        for (int i = 0; i < input.length; i++) {  //循环遍历输入数组input,依次以每个值为起始位置,依次计算到后面每个位置的最大面积
            //初始化一个当前遍历的i位置的值为最小值,循环遍历数组后续所有数值,依次计算到后续每个数值中的最小数值并计算以最小值为高的最大面积
            int min = input[i];
            for (int j = i; j < input.length; j++) {
                min = Math.min(min,input[j]);
                res = Math.max(res,(j-i+1)*min);
            }
        }

        return res;
    }
}

解法二:

public class test0402 {

    public static void main(String[] args) {

        int[] input = {3,2,5,4,6,1,4,2};
        int res = maxArea(input, 0, input.length);
        System.out.println(res);
    }

    private static int maxArea(int[] input,int start,int end){

        //当始末指针相等时,就返回0
        if (start == end){
            return 0;
        }

        //先将最小值指针设为起始位置,循环遍历始末指针之间的数组找到最小值的位置
        int min = start;
        for (int i = start; i < end; i++) {
            if (input[i]<input[min]){
                min = i;
            }
        }

        //获取以当前子数组最小值为高,始末位置差为宽的矩形面积
        int area = input[min]*(end-start);
        //计算最小值左侧与右侧的最大面积,比较最小值左右侧面积及当前子数组面积的最大值并返回
        int leftArea = maxArea(input,start,min);
        int rightArea = maxArea(input,min+1,end);

        int max = Math.max(area, leftArea);

        return Math.max(max,rightArea);
    }
}

解法三:

import java.util.Stack;

public class test0403 {

    public static void main(String[] args) {

        int[] input = {3,2,5,4,6,1,4,2};
        int res = maxArea(input);
        System.out.println(res);

    }

    private static int maxArea(int[] input){

        Stack<Integer> stack = new Stack<>();
        int area = 0;
        stack.push(-1);

        for (int i = 0; i < input.length; i++) {
            while (stack.peek()!=-1 && input[i]<=input[stack.peek()]){
                Integer pop = stack.pop();
                if (input[i]<input[pop]){
                    area = Math.max(area,(i-stack.peek()-1)*input[pop]);
                }else if (input[i]==input[pop]){
                    area = Math.max(area,(i-stack.peek())*input[pop]);
                }

            }
            if (stack.peek() == -1 || input[i] > input[stack.peek()]){
                stack.push(i);
            }
        }

        while (stack.peek()!=-1){
            Integer pop = stack.pop();
            area = Math.max(area,(input.length-stack.peek()-1)*input[pop]);
        }

        return area;
    }
}

40、矩阵中的最大矩形

题目:请在一个由0、1组成的矩阵中找出最大的只包含1的矩形并输出它的面积。例如,在图1的矩阵中,最大的只包含1的矩阵如阴影部分所示,它的面积是6。

数据结构与算法系列二之链表、哈希表及栈-LMLPHP

import java.util.Stack;

public class test0501 {

    private static int maxArea(int[][] input){

        if (input[0].length == 0){
            return 0;
        }

        int area = 0;
        int[] contains = new int[input[0].length];

        for (int[] ints : input) {
            for (int j = 0; j < input[0].length; j++) {
                if (ints[j] == 0) {
                    contains[j] = 0;
                } else {
                    contains[j]++;
                }
            }
            area = Math.max(area, getMaxArea(contains));
        }

        return area;
    }

    private static int getMaxArea(int[] in){

        Stack<Integer> stack = new Stack<>();
        int area = 0;

        for (int i = 0; i < in.length; i++) {
            while (!stack.empty() && in[stack.peek()]>in[i]){
                Integer pop = stack.pop();
                area = Math.max(area,(i-stack.peek()-1)*in[pop]);
            }
            if (!stack.empty() && in[stack.peek()]==in[i]){
                Integer pop = stack.pop();
                area = Math.max(area,(i-stack.peek())*in[pop]);
            }else{
                stack.push(i);
            }
        }

        while (!stack.empty()){
            Integer pop = stack.pop();
            area = Math.max(area,(in.length-pop-1)*in[pop]);
        }

        return area;
    }
}
10-22 18:47