《剑指offer》各编程题Java版分析 -- 数据结构

169 阅读6分钟

面试题3 -- 数组中重复的数字

题目一:找出数组中重复的数字

在一个长度为n的数组里的所有数字都在0~n-1范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是重复的数字2或者3。

时间复杂度O(n),空间复杂度O(n)的解法

public class Solution {
    public boolean duplicate(int[] numbers, int length, List<Integer> duplication) {
        if(nums == null || nums.length == 0 || nums.length == 1) {
            return false;
        }
        boolean duplicate = false;
        Map<Integer, Integer> dict = new HashMap<>(length);
        for(int num : numbers) {
            if(dict.containsKey(num)) {
                duplication.add(num);
                duplicate = true;
            } else {
                dict.put(num, 1);
            }
        }
        return duplicate;
    }
}

时间复杂度O(n),空间复杂度O(1)的解法

public class Solution {

	public boolean duplicate(int[] numbers, int length, List<Integer> duplication) {
        if (numbers == null || numbers.length == 0 || numbers.length == 1) {
            return false;
        }
        for (int i = 0; i < length; i++) {
            if (numbers[i] < 0 || numbers[i] >= length) {
                return false;
            }
        }
        boolean duplicate = false;
        for(int i = 0; i < length; i++) {
            while(numbers[i] != i) {
                int target = numbers[i];
                if(numbers[target] == target) {
                    duplicate = true;
                    duplication.add(target);
                    break;
                } else {
                    int temp = numbers[target];
                    numbers[target] = numbers[i];
                    numbers[i] = temp;
                }
            }
        }
        return duplicate;
    }
}

题目二:不修改数组找出重复的数字

在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数组,但不能修改输入的数组。例如,如果输入长度为8的数组{2,3,5,4,3,2,6,7},那么对应的输出是重复的数字2或者3。

首先,很容易想到时间复杂度O(n)、空间复杂度O(n)的hash解法,同上。
之后,书中提到了可以利用 二分法 的思想,结合具体的例子 举例 分析。长度为8的数组,所有数字都在1~7,通过中间数字4把它分成1~4和5~7,分别统计各个区间的个数,然后发现1~4区间多了,则再对1~4区间进行二分...

public int getDuplication(int[] numbers, int length) {
        if (numbers == null || numbers.length <= 1) {
            return -1;
        }
        int left = 1;
        int right = length - 1;
        int mid = (left + right) >> 1;
        while(left < right) {
            int leftCount = 0;
            int rightCount = 0;
            for (int i = 0; i < length; i++) {
                if(numbers[i] >= left && numbers[i] <= mid) {
                    leftCount++;
                } else if(numbers[i] > mid && numbers[i] <= right) {
                    rightCount++;
                }
            }
            if (leftCount > mid - left + 1) {
                right = mid;
            } else if (rightCount > right - mid) {
                left = mid+1;
            }
            mid = (left + right) >> 1;
        }
        return mid;
    }

面试题4 -- 二维数组中的查找

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

1 2 8 9
2 4 9 12
4 7 10 13
6 8 11 15

此题Java与书中C++的实现方式没有太多区别,一种“俄罗斯方块”开心消消乐的思想的。笔者一开始想到了二分查找,但是显然不行,不再赘述。

public class Solution {
    public boolean find(int[][] matrix, int number) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }
        int row = 0;
        int col = matrix[0].length - 1;
        while(row < matrix.length && col >= 0) {
            if(number < matrix[row][col]) {
                col--;
            } else if(number > matrix[row][col]) {
                row++;
            } else {
                return true;
            }
        }
        return false;
    }
}

面试题5 -- 替换空格

题目:请事先的一个函数,把字符串中的每个空格替换成"%20"。例如,输入"We are happy.",则输出"We%20are%20happy."。

这个题的重点在于 是否是在原来的字符串上进行替换。如果不是原地修改,那这道题就没什么意义了。

C++中的字符串就是字符数组,即一串字符的其实指针,通过'\0'表示结尾。如"We are happy.",实际是"We are happy.\0"。
这点Java和C++略有不同,Java中的String是JDK封装好的不可修改的字符数组。无法实现原地修改。所以我们直接改用char[]

public class Solution {
    public void replaceBlank(char[] str, int length) {
        if (str == null || str.length == 0) { // 一定不要忘记考虑边界情况!!!写逻辑之前先记个TODO,写完正文回来补
            return;
        }
        int spaces = 0;
        for (int i = 0; i < str.length; i++) {
            if (str[i] == ' ') {
                spaces++;
            }
        }
        int pOld = length - 1;
        int pNew = length - 1 + (spaces * 2);
        while(pOld >= 0) {
            if (str[pOld] != ' ') {
                str[pNew--] = str[pOld--];
            } else {
                str[pNew--] = '0';
                str[pNew--] = '2';
                str[pNew--] = '%';
                pOld--;
            }
        }
    }
}

面试题5相关题目

有两个排序的数组A1和A2,内存在A1的末尾有足够多的空余空间容纳A2。请实现一个函数,把A2中的所有数字插入A1中,并且所有的数字是排序的。

思路和上面一样,也是考虑 从后往前 遍历。

public class MergeSortSolution {
    public void mergeSort(int[] a1, int[] a2, int len1, int len2) {
        if(a1 == null || a2 == null) {
            return;
        }
        int p = len1 + len2 - 1;
        int p1 = len1 - 1;
        int p2 = len2 - 1;
        while(p1 >= 0 && p2 >= 0) {
            if(a1[p1] > a2[p2]) {
                a1[p--] = a1[p1--];
            } else {
                a1[p--] = a2[p2--];
            }
        }
        while(p1 >= 0) { a1[p--] = a1[p1--] };
        while(p2 >= 0) { a1[p--] = a2[p2--] };
    }
}

题目6 -- 从尾到头打印链表

题目:输入一个链表的头节点,从尾到头反过来打印出每个节点的值。

典型的“后进先出”,用自定义的 或者递归方法栈实现。考察的是栈这种数据结构的理解与运用。

public void revertPrint(ListNode head) {
    Deque<Integer> stack = new LinkedList<>();
    ListNode node = head;
    while (node != null) {
        stack.push(node.key);
        node = node.next;
    }
    while (!stack.isEmpty()) {
        System.out.println(stack.pop());
    }
}

public void revertPrint2(ListNode head) {
    if (head == null) {
        return;
    }
    revertPrint(head.next);
    System.out.println(head.key);
}

题目7 -- 重建二叉树

题目:输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如,输入前序遍历{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建如图所示的二叉树并输出它的头节点。

考察的是二叉树,题目是本科数据结构这门课的经典题型,前序遍历 + 中序遍历 ==> 二叉树。如下图,根据前序遍历的信息获取根节点,根据中序遍历的信息获取左右字数,然后递归的处理完。

public BinaryTreeNode reBuildBinaryTree(int[] preorder, int[] inorder) {
    if (preorder == null || inorder == null || preorder.length != inorder.length) {
        return null;
    }
    return doRebuildBinaryTree(preorder, inorder, 0, 0, preorder.length);
}

private BinaryTreeNode doRebuildBinaryTree(int[] preorder, int[] inorder, int left1, int left2, int length) {
    if (length <= 0) {
        return null;
    }
    int root = preorder[left1];
    for (int i = 0; i < length; i++) {
        int mid = left2 + i;
        if (root == inorder[mid]) {
            BinaryTreeNode node = new BinaryTreeNode();
            node.value = root;
            node.left = doRebuildBinaryTree(preorder, inorder,
                    left1 + 1, left2, mid - left2);
            node.right = doRebuildBinaryTree(preorder, inorder,
                    left1 + mid - left2 + 1, mid + 1, left2 + length - 1 - mid);
            return node;
        }
    }
    // find no root, input data is wrong
    throw new IllegalArgumentException("find no root, input data is wrong.");
}

题目8 -- 二叉树的下一个节点

题目:给定一颗二叉树和其中的一个节点,如何找出中序遍历的下一个节点?树中的节点除了有两个分别指向左右子节点的指针,还有一个指向父节点的指针。

此题的关键点在于有一个指向父节点的指针,因此可以考虑结合例子分析中序遍历的过程,分情况讨论中序遍历中下一个节点的可能性。

  1. 当前节点有右指针,则下一个节点是其右孩子的最后一个左孩子
  2. 当前节点没有右指针,但是当前节点是一个左指针,则下一个节点是其双亲
  3. 当前节点没有右指针,并且当前节点自己是一个右指针,则需要一直顺着其双亲往上找,找到第一个是左节点的双亲,然后返回其双亲。没有找到这样的节点的话,则返回null表示当前节点已经是中序遍历的末尾了。

interview8.gif

public class Solution {

    public BinaryTreeNode findInOrderNext(BinaryTreeNode root, BinaryTreeNode node) {
        if (root == null || node == null) {
            return null;
        }
        if (node.right != null) { // 场景1:有右孩子
            BinaryTreeNode next = node;
            while(next.left != null) {
                next = next.left;
            }
            return next;
        } else if(isLeftChild(node)) { // 场景2:是左孩子
            return node.parent;
        } else { // 场景3:自己是右孩子,且没有右孩子
            BinaryTreeNode father = node.parent;
            while(father != null) {
                if (isLeftChild(father)) {
                    return father.parent;
                }
                father = father.parent;
            }
            return null;
        }
    }

    private boolean isLeftChild(BinaryTreeNode node) {
        if (node == null || node.parent == null) {
            return false;
        }
        return node == node.parent.left;
    }
}

题目9 -- 用两个栈实现队列

题目:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数appendTail和deleteHead,分别完成在队列尾部插入节点和在队列头部删除节点的功能。

interview9.gif

/**
 * 两个栈实现一个队列
 **/
public class StackQueue<T> {

    private Deque<T> appendStack;
    private Deque<T> popStack;

    public StackQueue() {
        appendStack = new LinkedList<>();
        popStack = new LinkedList<>();
    }

    /**
     * maybe OOM
     **/
    public void appendTail(@Nullable T elem) {
        appendStack.push(elem);
    }

    public T deleteHead() {
        if (popStack.isEmpty()) {
            while(!appendStack.isEmpty()) {
                T elem = appendStack.pop();
                popStack.push(elem);
            }
        }
        if (popStack.isEmpty()) {
            throw new IllegalStateException("queue is empty.");
        }
        return popStack.pop();
    }

    public static void main(String[] args) {
        StackQueue<Integer> queue = new StackQueue<>();
        queue.appendTail(1);
        queue.appendTail(2);
        System.out.println(queue.deleteHead());
        queue.appendTail(3);
        System.out.println(queue.deleteHead());
        System.out.println(queue.deleteHead());
        System.out.println(queue.deleteHead());
        System.out.println(queue.deleteHead()); // exception
    }

}

相关题目:用两个队列实现一个栈。

interview9_2.gif

/**
 * 两个队列实现一个栈
 **/
public class QueueStack<T> {

    private Queue<T> queue1;
    private Queue<T> queue2;

    public QueueStack() {
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }

    public void push(T elem) {
        Queue<T> curQueue = queue2.isEmpty() ? queue1 : queue2;
        curQueue.offer(elem);
    }

    public T pop() {
        Queue<T> curQueue = queue2.isEmpty() ? queue1 : queue2;
        Queue<T> anotherQueue = curQueue == queue1 ? queue2 : queue1;
        if (curQueue.isEmpty()) {
            throw new IllegalStateException("stack is empty.");
        }
        while(curQueue.size() > 1) {
            T elem = curQueue.poll();
            anotherQueue.offer(elem);
        }
        return curQueue.poll();
    }

    public static void main(String[] args) {
        QueueStack<Integer> stack = new QueueStack<>();
        stack.push(1);
        System.out.println(stack.pop());
        stack.push(2);
        stack.push(3);
        System.out.println(stack.pop());
        System.out.println(stack.pop());
        System.out.println(stack.pop());
    }

}