面试题49:丑数

题目:我们把只包含因子2、3和5的数称作丑数(Ugly Number)。求按从小到大的顺序的第1500个丑数。例如6、8都是丑数,但14不是,因为它包含因子7。习惯上我们把1当做第一个丑数。

  • 算法:暴力
    • 丑数在一直除2一直除3一直除5后结果是1
public int nthUglyNumber(int n) {
    checkArguments(n);

    int number = 0;
    int uglyIndex = 0;
    while (uglyIndex < n) {
        number++;
        if (isUglyNumber(number)) {
            uglyIndex++;
        }
    }
    return number;
}

public boolean isUglyNumber(int n) {
    while ((n & 1) == 0) {
        n >>= 1;
    }
    while (n % 3 == 0) {
        n /= 3;
    }
    while (n % 5 == 0) {
        n /= 5;
    }
    return n == 1;
}
  • 算法:空间换时间
    • 保存结果到数组中
    • 下一个丑数一定是前面的丑数分别乘2乘3乘5中的最小但又大于目前最大丑数的值
    • 遍历的同时分别找到乘2乘3乘5刚好大于目前最大丑数的位置
public int nthUglyNumber(int n) {
    checkArguments(n);

    int[] uglyArray = new int[n];
    uglyArray[0] = 1;
    int multiply2Index = 0;
    int multiply3Index = 0;
    int multiply5Index = 0;
    for (int i = 1; i < n; i++) {
        uglyArray[i] = Math.min(Math.min(uglyArray[multiply2Index]*2, uglyArray[multiply3Index]*3), uglyArray[multiply5Index]*5);
        if (uglyArray[multiply2Index] * 2 == uglyArray[i]) {
            multiply2Index++;
        }
        if (uglyArray[multiply3Index] * 3 == uglyArray[i]) {
            multiply3Index++;
        }
        if (uglyArray[multiply5Index] * 5 == uglyArray[i]) {
            multiply5Index++;
        }
    }
    return uglyArray[n-1];
}

面试题50:第一个只出现一次的字符

题目一:字符串中第一个只出现一次的字符

题目:在字符串中找出第一个只出现一次的字符。如输入"abaccdeff",则输出'b'。

  • 算法:基于数组创建简单的哈希表
    • 键是ASCII码
    • 值是字符出现的次数
public char firstUniqChar(String s) {
    checkArguments(s);

    int[] hashTable = new int[256];
    for (char c : s.toCharArray()) {
        hashTable[c]++;
    }
    for (char c : s.toCharArray()) {
        if (hashTable[c] == 1) {
            return c;
        }
    }
    return ' ';
}

题目二:字符流中第一个只出现一次的字符

题目:请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是'g'。当从该字符流中读出前六个字符"google"时,第一个只出现一次的字符是'l'。

class  CharStatistics {
    // occurrence[ch] = 0: ch has not been found
    // occurrence[ch] >= 1: ch has been found one times and occurrence[ch] is the index+1 of ch in string stream 
    // occurrence[ch] = -1: ch has been found for multiple times
    private int[] occurrence = new int[256];
    private int index = 1;
    public void insert(char ch) {
        if (occurrence[ch] == 0) {
            occurrence[ch] = index++;  
        }
        if (occurrence[ch] >= 1) {
            occurrence[ch] = -1;
        }
    }
    public char firstAppearingOnce() {
        char result = ' ';
        int minIndex = Integer.MAX_VALUE;
        // 因为字符流随时间增长可能变得很长,所以选择遍历数组而不是遍历当前的字符流。时间复杂度就是O(1)
        for (int i = 0; i < 256; i++) {
            if (occurrence[i] >= 1 && occurrence[i] < minIndex) {
                result = (char) i;
                minIndex = occurrence[i];
            }
        }
        return result;
    }
}

面试题51:数组中的逆序对

题目:在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

  • 算法:归并排序
    • nums数组和copy数组在递归时掉包了
    • 每次在
      left = reversePairs(copy, nums, start, start+middle);right = reversePairs(copy, nums, start+middle+1, end);函数中对nums数组排序后,在reversePairs(nums, copy, start, middle, end)函数中就可以利用左右有序计算逆序对
    • 返回最后结果时,copy数组是有序的,nums数组是左右两边分别有序的
public int reversePairs(int[] nums) {
    checkArguments(nums);

    int[] copy = new int[nums.length];
    System.arraycopy(nums, 0, copy, 0, nums.length);
    return reversePairs(nums, copy, 0, nums.length-1);
}

public int reversePairs(int[] nums, int[] copy, int start, int end) {
    if (start == end) {
        copy[start] = nums[start];
        return 0;
    } else {
        int middle = (end - start) >> 1;
        int left = reversePairs(copy, nums, start, start+middle);
        int right = reversePairs(copy, nums, start+middle+1, end);
        return left + right + reversePairs(nums, copy, start, middle, end);
    }
}

public int reversePairs(int[] nums, int[] copy, int start, int middle, int end) {
    int i = start + middle;
    int j = end;
    int copyIndex = end;
    int count = 0;
    while (i >= start && j >= start+middle+1) {
        if (nums[i] > nums[j]) {
            copy[copyIndex--] = nums[i--];
            count += (j - start - middle);
        } else {
            copy[copyIndex--] = nums[j--];
        }
    }
    for (;i >= start; i--) {
        copy[copyIndex--] = nums[i];
    }
    for (;j >= start+middle+1; j--) {
        copy[copyIndex--] = nums[j];
    }
    return count;
}

public void checkArguments(int[] array) {
    if (array == null || array.length <= 1) {
        throw new RuntimeException("Invalid array");
    }
}

面试题52:两个链表的第一个公共结点

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

  • 算法:
    • LeetCode Discuss
    • 使用两个指针进行遍历,当一个指针到头时,就重置它为另一个链表的头节点
    • 当两个链表长度相同时:不到一轮遍历既能找到答案
    • 当两个链表长度不同时:不到二轮遍历既能找到答案
    public ListNode getIntersectionNode(ListNode l1, ListNode l2) {
        // checkArguments(l1, l2); // l1 == null; l2 == null;
        ListNode move1 = l1;
        ListNode move2 = l2;
        while (move1 != move2) {
            move1 = (move1 == null ? l2 : move1.next);
            move2 = (move2 == null ? l1 : move2.next);
        }
        return move1;
    }
  • 算法:
    • 首先遍历计算两个链表长度之差lengthDiff
    • 然后长链表先走lengthDiff步
    • 最后一起遍历直到找到公共结点
    • O(m+n)
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    checkArguments(headA);
    checkArguments(headB);

    int lengthA = getLengthOfList(headA);
    int lengthB = getLengthOfList(headB);
    int lengthDiff = lengthA > lengthB ? (lengthA - lengthB) : (lengthB - lengthA);
    ListNode longList = lengthA > lengthB ? headA : headB;
    ListNode shortList = lengthA > lengthB ? headB : headA;
    for (int i = 0; i < lengthDiff; i++) {
        longList = longList.next;
    }
    while (longList != null && shortList != null && (longList != shortList)) {
        longList = longList.next;
        shortList = shortList.next;
    }
    return longList;
}

public int getLengthOfList(ListNode head) {
    int length = 0;
    while (head != null) {
        length++;
        head = head.next;
    }
    return length;
}
  • 算法:
    • 使用两个辅助栈把两个链表结点顺序入栈
    • 这样出栈时就可以从尾部往头部遍历结点以此找到公共结点
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    checkArguments(headA);
    checkArguments(headB);

    Stack<ListNode> stackA = new Stack<>();
    Stack<ListNode> stackB = new Stack<>();
    while (headA != null) {
        stackA.push(headA);
        headA = headA.next;
    }
    while (headB != null) {
        stackB.push(headB);
        headB = headB.next;
    }

    ListNode result = null;
    while (!stackA.isEmpty() && !stackB.isEmpty()) {
        ListNode nodeA = stackA.pop();
        ListNode nodeB = stackB.pop();
        if (nodeA != nodeB) {
            return result;
        }
        result = nodeA;
    }
    return result;
}