剑指offer题解数据结构篇

文章目录

  • 数组篇
    • 剑指 Offer 03. 数组中重复的数字
      • 题目
      • 题解1:排序法
      • 题解2:哈希表
      • 题解3:手动维护哈希表
    • 剑指 Offer 04. 二维数组中的查找
      • 题目
      • 题解1:暴力法
      • 题解2:线性查找法
  • 字符串篇
    • 剑指 Offer 05. 替换空格
      • 题目
      • 题解
  • 链表篇
    • 剑指 Offer 06. 从尾到头打印链表
      • 题目
      • 题解
  • 二叉树篇
    • 剑指 Offer 07. 重建二叉树
      • 题目
      • 题解
  • 栈和队列篇
    • 剑指 Offer 09. 用两个栈实现队列
      • 题目
      • 题解

数组篇

剑指 Offer 03. 数组中重复的数字

题目

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

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

题解1:排序法

先把输出数组排序,从排序的数组中找出重复数字只需从头到尾扫描排序后的数组即可。

class Solution {
    public int findRepeatNumber(int[] nums) {
        Arrays.sort(nums);
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] == nums[i - 1]) 
                return nums[i];
        }
        return -1;
    }
}

在本方法中,对数组进行排序的时间复杂度为O(n·logn),遍历数组的时间复杂度为O(n),所以最终的时间复杂度为O(n·logn),空间复杂度为O(1)。

题解2:哈希表

从头到尾按顺序扫描整个数组的数字,每扫描到一个数字的时候,都可以用O(1)的时间来判断哈希表里是否包含了该数字,若哈希表中还没有这个数字,就将其加入到哈希表中,若有,那就get到结果了。

class Solution {
    public int findRepeatNumber(int[] nums) {
        HashSet<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;
    }
}

在本方法中,由于在哈希表中的查找的时间复杂度为O(1),所以该方法的时间复杂度为O(n),但其提高了时间复杂度的同时,是以开辟一个哈希表为代价的,空间复杂度为O(1)。

题解3:手动维护哈希表

这道题存在一个细节,就是数组长度为n,然后数组中的数字都是在0 ~ n-1的范围内,这也就是说,如果数组中没有重复元素,那么数组中的n个元素,一定分别为01、……、n-1。而如果没有重复的元素,那这些元素如果排序的话,其数组中的值恰恰等于索引值!所以我们可以考虑手动维护一个哈希表,将每个元素哈希映射到其该在的位置上。

从头到尾依次扫描数组中的每个数字n,当扫描到下标为i 的数字时 ,首先比较这个n是否等于i

  • 如果是,则扫描下一个数字;
  • 如果不是,则将它和第n个数字进行比较:
    • 如果它和第n个数字相等,就找到了一个重复的数字;
    • 如果不相等,就把第i个数字和第n个数字交换,也就是吧数字n(第i个数字)放到它该在的位置上去

重复上述比较,直到我们发现重复数字就可以返回结果,若遍历整个数组都没有,那么就是没有重复的数字。

class Solution {
    public int findRepeatNumber(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            //用while表示直到将其交换到其最终位置
            while (nums[i] != i) {
                if (nums[nums[i]] == nums[i])
                    return nums[i];
                swap(nums, i, nums[i]);
            }
        }
        return -1;
    }
    public void swap(int[] nums, int i1, int i2) {
        int tmp = nums[i1];
        nums[i1] = nums[i2];
        nums[i2] = tmp;
    }
}

在本方法中,时间复杂度为O(n),空间复杂度也优化为了O(1)。

剑指 Offer 04. 二维数组中的查找

题目

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

现有矩阵 matrix 如下:
[
  [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]
]
给定 target = 5,返回 true。
给定 target = 20,返回 false。

题解1:暴力法

class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                if (matrix[i][j] == target) return true;
            }
        }
        return false;
    }
}

暴力法虽然简单,但是其时间复杂度为O(n^2),查找太慢

题解2:线性查找法

由于矩阵具有每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序的特点,如果我们首先选取矩阵中右上角的数字:

  • 如果该数字num等于要查找数字target,则查找过程结束;
  • 如果该数字num大于要查找数字target,那么由于每一列都按照从上到下递增,所以可以剔除num所在的列;
  • 如果该数字num小于要查找数字target,那么由于每一行都按照从上到下递增,所以可以剔除num所在的行。

用上述的查找方式,若数字target不在右上角的话,那么在该查找过程中,每次删选都会剔除一整行或一整列,这就大幅减少了查找的时间。

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 r = 0, c = columns - 1;
        while (r < rows && c >= 0) {
            if (matrix[r][c] == target) return true;
            else if (matrix[r][c] > target) c--;
            else r++;
        }
        return false;
    }
}

字符串篇

剑指 Offer 05. 替换空格

题目

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。示例:

输入:s = "We are happy."
输出:"We%20are%20happy."

题解

这题作为Java语言实现其实没啥好说的,Java字符串也不像C字符串一样由\0结尾,这道题我就直接用StringBuilder可变数组做了:

class Solution {
    public String replaceSpace(String s) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == ' ') sb.append("%20");
            else sb.append(s.charAt(i));
        }
        return sb.toString();
    }
}

链表篇

首先定义链表的数据结构如下:

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

剑指 Offer 06. 从尾到头打印链表

题目

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

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

题解

由于题目要求从头到尾打印链表,也就是说是反着遍历链表,这无疑应该让我们想到先进后出的数据结构——,所以本题也自然而然的想到用栈来解决问题:

class Solution {
    public int[] reversePrint(ListNode head) {
        Deque<Integer> stack = new LinkedList<>();
        ListNode cur = head;
        while (cur != null) {
            stack.push(cur.val);
            cur = cur.next;
        }
        int[] res = new int[stack.size()];
        int i = 0;
        while(!stack.isEmpty()) res[i++] = stack.pop();
        return res;
    }
}

二叉树篇

首先定义二叉树的数据结构如下:

public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    
    TreeNode(int x) {this.val = x;}
}

剑指 Offer 07. 重建二叉树

题目

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。示例:

给出:
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
    3
   / \
  9  20
    /  \
   15   7

题解

先根据前序遍历的第一个数字创建根节点,然后在中序遍历序列中找到根节点的位置,这样就能够确定左、右子树节点的数量,在前序遍历和中序遍历序列中划分了左、右子树后,就可以调用递给函数继续构建其子树的左、右子树:

class Solution {
    int[] preorder;
    HashMap<Integer, Integer> map = new HashMap<>(); //存放元素在中序遍历中的索引
    
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        this.preorder = preorder;
        for (int i = 0; i < inorder.length; i++) {
            this.map.put(inorder[i], i);
        }
        return recursive(0, 0, inorder.length - 1);
    }

    public TreeNode recursive(int pre_root_idx, int in_left_idx, int in_right_idx) {
        if (in_left_idx > in_right_idx) return null;
        TreeNode root = new TreeNode(this.preorder[pre_root_idx]);
        int in_idx = map.get(this.preorder[pre_root_idx]); //得到根节点在中序序列中的索引
        root.left = recursive(pre_root_idx + 1, in_left_idx, in_idx - 1);
        root.right = recursive(pre_root_idx + (in_idx - in_left_idx + 1), in_idx + 1, in_right_idx);
        return root;
    }
}

栈和队列篇

剑指 Offer 09. 用两个栈实现队列

题目

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )。示例:

输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]

题解

用两个栈实现队列,我们可以用一个栈inStack只负责模拟入队,用另一栈outStack只负责模拟出队:

对于inStack模拟入队,就是每当有元素入队时,压入inStack即可;而对于outStack模拟出队:

  • outStack不为空时,在outStack栈顶的元素即为最先进入队列的元素,弹出就可以模拟出队;
  • outStack为空时,我们就把inStack中的元素逐个弹出并压入outStack中,直到inStack中没有元素。由于先进入队列的元素被压倒inStack的底端,所以经过弹出压入操作后就处于了outStack的顶端,所以outStack顶端就又是第一个进入队列的元素,弹出就可以模拟出队
class CQueue {

    Deque<Integer> inStack;
    Deque<Integer> outStack;

    public CQueue() {
        this.inStack = new LinkedList();
        this.outStack = new LinkedList();
    }
    
    public void appendTail(int value) {
        inStack.push(value);
    }
    
    public int deleteHead() {
        if (inStack.isEmpty() && outStack.isEmpty()) return -1;
        if (outStack.isEmpty()) {
            while (!inStack.isEmpty()) {
                outStack.push(inStack.pop());
            }
        }
        return outStack.pop();
    }
}

你可能感兴趣的:(算法,数据结构)