剑指Offer算法题

一、链表

1、从尾到头打印链表

问题:输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

输入:head = [1,3,2]
输出:[2,3,1]

解法:使用栈先进后出

class Solution {
    public int[] reversePrint(ListNode head) {
        //将链表数据依次放到栈中
        Stack<ListNode> stack = new Stack<ListNode>();
        ListNode p= head;
        while (p!= null) {
            stack.push(p);
            p= p.next;
        }

        //将栈中的数弹出放到数组中
        int size = stack.size();
        int[] print = new int[size];
        for (int i = 0; i < size; i++) {
            print[i] = stack.pop().val;
        }
        return print;
    }
}

2、删除链表的节点

问题:给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。

    输入: head = [4,5,1,9], val = 5
    输出: [4,1,9]

class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        //删除时添加dummy头节点,方便修改头节点
        ListNode dummy = new ListNode(-1);
        dummy.next=head;
        ListNode p=dummy;
        //找到值为val的节点
        while(p.next.val!=val){
            p=p.next;
        }
        //执行删除操作
        p.next=p.next.next;
        //返回dummy的下个节点
        return dummy.next;
        
    }
}

3、链表中倒数第k个节点

问题:输入一个链表,输出该链表中倒数第k个节点。

  输入:给定一个链表: 1->2->3->4->5, 和 k = 2.

  输出:返回链表 4->5.

class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode fast = head;
        ListNode slow = head;
        //先让快指针fast前进k步
        while (fast != null && k > 0) {
            fast = fast.next;
            k--;
        }
        //快慢指针同时前进n-k,慢指针的位置即倒数k的位置
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }

        return slow;
    }
}

 4、链表中环的入口位置

问题:

给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

解法:使用双指针

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head, slow = head;
        while (true) {
            if (fast == null || fast.next == null) return null;
            //快指针每次走两步,慢指针每次走一步
            fast = fast.next.next;
            slow = slow.next;
            //当快慢指针相遇时,退出循环
            if (fast == slow) break;
        }
        //令指针从头开始走,当两个指针相遇时即环的入口
        fast = head;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
}

5、反转链表

给定单链表的头节点 head ,请反转链表,并返回反转后的链表的头节点。

    输入:head = [1,2,3,4,5]
    输出:[5,4,3,2,1]

解法一:迭代、多指针

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            //先保存当前节点的下个节点
            ListNode next = curr.next;
            //将当前节点的next指向上一节点
            curr.next = prev;
            //移动pre到curr
            prev = curr;
            //移动curr到下个节点
            curr = next;
        }
        return prev;
    }
}

解法二:递归

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        //设置新的头节点为反转后的链表
        ListNode newHead = reverseList(head.next);
        //修改一下节点的next为当前节点
        head.next.next = head;
        //将当前节点的next置为null
        head.next = null;
        //返回最后一个节点为新的链表头节点
        return newHead;
    }
}

 6、合并两个排序的链表

问题:输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

  输入:1->2->4, 1->3->4
  输出:1->1->2->3->4->4

解法一:迭代

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0), cur = dummy;
        while(l1 != null && l2 != null) {
            //当l1的值比较小,curr的next指向l1
            if(l1.val < l2.val) {
                cur.next = l1;
                l1 = l1.next;
            }
            //当l2的值比较小,curr的next指向l2
            else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        //将curr的next指向当前不为空的链表
        cur.next = l1 != null ? l1 : l2;
        return dummy.next;
    }
}

解法二:递归

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        //当l1为空,直接返回l2链表
        if(l1 == null) {
            return l2;
        }
        if(l2 == null) {
            return l1;
        }
        //如果l1的值比较小,则设置l1的next为l1.next和l2中更小的那个节点
        if(l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

7、复杂链表的复制

问题:请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

  输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
  输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

解法:回溯 + 哈希表

class Solution {
    Map<Node, Node> cachedNode = new HashMap<Node, Node>();

    public Node copyRandomList(Node head) {
        if (head == null) {
            return null;
        }
        //当缓存中没有当前节点
        if (!cachedNode.containsKey(head)) {
            //先创建新节点
            Node headNew = new Node(head.val);
            //把当前节点和新节点的映射关系放到缓存中
            cachedNode.put(head, headNew);
            //新节点的next和random都通过递归从缓存获取
            headNew.next = copyRandomList(head.next);
            headNew.random = copyRandomList(head.random);
        }
        return cachedNode.get(head);
    }
}

8、两个链表的第一个公共节点

问题:输入两个链表,找出它们的第一个公共节点。

  输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
  输出:Reference of the node with value = 8

解法一:哈希集合

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Set<ListNode> visited = new HashSet<ListNode>();
        //先遍历链表A,把节点放入set中
        ListNode p = headA;
        while (p != null) {
            visited.add(p);
            p = p.next;
        }
        //再遍历链表B
        p = headB;
        while (p != null) {
            //当集合中存在当前节点,该节点即为两个链表首次相交的节点
            if (visited.contains(p)) {
                return p;
            }
            p = p.next;
        }
        return null;
    }
}

解法二:双指针

public class Solution {

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        //pA和pB先后遍历两个链表,pA先从链表A的头节点开始走
        ListNode pA = headA, pB = headB;
        //当pA和pB相等,即相交的节点
        while (pA != pB) {
            //当pA为空时,继续遍历链表B
            pA = pA != null ? pA.next : headB;
            pB = pB != null ? pB.next : headA;
        }
        return pA;
    }
}

二、数组

1、数组中重复的数字

找出数组中重复的数字。在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

  输入:[2, 3, 1, 0, 2, 5, 3]
  输出:2 或 3

解法一:哈希集合

 

public int findRepeatNumber(int[] nums) {
    Set<Integer> set = new HashSet<>();
    for (int i = 0; i < nums.length; i++) {
        //判断当前元素是否已经存在
        if (set.contains(nums[i])) {
            return nums[i];
        }
        set.add(nums[i]);
    }
    return -1;
}

解法二:将元素i放到i位置

public int findRepeatNumber(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        //判断索引i是否等于索引i位置的值,例如索引2的值也为2
        while (i != nums[i]) {
​            //判断索引nums[i]是否等于索引nums[i]位置的值,是的话说明该位置已经交换完毕了,存在重复值
            if (nums[i] == nums[nums[i]]) {
                return nums[i];
            }
​
            //交换nums[i]和索引nums[i]上的值
            int tmp = nums[nums[i]];
            nums[nums[i]] = nums[i];
            nums[i] = tmp;
        }
    }
    //没有找到重复数字
    return -1;
}

 2、二维数组中的查找

问题:在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

现有矩阵 matrix 如下,给定 target = 5,返回 true。

[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]

解法:从右上角开始搜索

class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        int rows = matrix.length, columns = matrix[0].length;
        //从右上角开始搜索,能保证每次判断只需往一个方向移动
        int row = 0, column = columns - 1;
        //当走到数组尽头表示没找到
        while (row < rows && column >= 0) {
            int num = matrix[row][column];
            //找到查找的数
            if (num == target) {
                return true;
            //如果当前值比目标值大,则向左移动
            } else if (num > target) {
                column--;
            //如果当前值比目标值小,则向下移动
            } else {
                row++;
            }
        }
        return false;
    }
}

 

3、旋转数组的最小数字

问题:

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为1。  

  输入:[3,4,5,1,2]
  输出:1

解法:二分法

class Solution {
    //正常递增时,左边数字一定小于右边,如果存在某个数比有右边的数大,说明这段区间存在旋转数字
    public int minArray(int[] numbers) {
        int l = 0, r = numbers.length - 1;
        while (l < r) {
            int mid = (l + r) / 2;
            //当中点值比右边界值大,说明最小值在右边区间
            if (numbers[mid] > numbers[r]) l = mid + 1;
            //当中点值比右边界值小,说明最小值在左边区间
            else if (numbers[mid] < numbers[r]) r = m;
            //如果中点值与右边界值相等,则区间想左移动
            else r--;
        }
        //左右相等时,返回的为最小值
        return numbers[l];
    }
}

 

posted @ 2022-01-16 15:28  安小  阅读(43)  评论(0编辑  收藏  举报