Array & List
// 排序 int[] array = {3, 4, 1, 2, 5}; Arrays.sort(array); // T = O(n * log2(n)) List<Integer> list = Arrays.asList(array); Collections.sort(list); // 使用ArrayList的话,T = O(n * log2(n)) list.sort(null); // 上面的函数调用这个函数,其内部实现方法是先把列表转换为数组再排序 // 因为如果是链表,直接排序的时间复杂度为O((n ^ 2) * log2(n))。 // array和list转换 List<String> list = Arrays.asList(array); String[] array = list.toArray(new String[0]); // max & min Map<String, Integer> map = ...; // 找出具有最大value值的key String max = Collections.max( map.entrySet(), Comparator.comparingInt(Map.Entry<String, Integer>::getValue)).getKey(); // 或者 max = Collections.max( map.entrySet(), Map.Entry.comparingByValue()).getKey(); // 这些其实没什么必要,面试时记不住就用循环解决 // string String str = "aaa bbb ccc"; String[] parts = str.split(" "); // {"aaa", "bbb", "ccc"} String[] parts1 = str.split(" ", 2); // {"aaa", "bbb ccc"} String[] parts2 = str.split(" ", -1); // -1的意思是不要删除最后面的空字符
另外,Java的Collection由于只接受对象形式的元素(就是说不能用基本数据类型的元素),加上其维护代码的开销,性能往往比数组差。比如int[]比ArrayList<Integer>要快,尤其是在比拼算法所用时间的情况下尤为明显。
Stack
“Stack”是Java中关于栈的旧API。现在应该使用Deque。LIFO。
Deque<String> stack = new ArrayDeque<>(); stack.push("abc"); String s1 = stack.peek(); String s2 = stack.pop();
Queue
普通的队列,在Java中一般是以LinkedList的方式实现的。Queue常用于广度优先搜索算法。FIFO。
Queue<String> queue = new LinkedList<>(); queue.offer("abc"); // enqueue String s = queue.poll(); // dequeue
Priority Queue
Priority Queue常被用来在不排序的情况下取最大的或最小的几个值。如果有n个数据元素,要取前k个(k<n)最小的元素,用排序法的话T=O(n * log2(n)),而用Priority Queue的话,则为T=O(n * log2(k)),这样就能快一些。方法是往queue里逐个放入数据元素,如果queue的长度大于k,则把一个元素移出(也就是最大的那个或最小的那个,取决于Priority Queue的设定方式。这样,剩下的就是k个最小的或最大的元素。
Priority Queue的时间复杂度:enqueue和dequeue操作为O(log2(k)),查看(peek)最前面的元素为O(1)。
PriorityQueue<Elem> queue = new PriorityQueue<>(Comparator.comparingInt(Elem::getValue)); for (Elem elem : list) { queue.offer(elem); if (queue.size() > k) { queue.poll(); } }
上面的代码中的queue.poll()会把Elem.getValue()返回值最小的元素移出。要想移出最大的,则改为Comparator.comparingInt(elem -> -elem.getValue())。
另一个应用的例子:合并k个有序列表。一般的算法为取各列表的第一个元素,找到最小的那个,加入新列表,然后把最小元素的下一个元素作为它原所属列表的第一个元素。不断重复,直到用完所有列表的元素。这样做的话,T=O(N * k),其中N为所有列表中的元素总数,k为列表个数。因为每次从k各元素中取最小值为O(k),而对所有元素都要进行一次这样的操作,故为O(N * k)。
可以用Priority Queue进行优化:把各列表的第一个元素加入queue;取出最前面的元素加入新列表,并把它的下一个元素加入queue。这样的话,每次enqueue和dequeue操作为O(log2(k)),对所有元素做这种操作,总体的时间复杂度T=O(N * log2(k)),比上述的O(N * k)要快。
public ListNode mergeKLists(ListNode[] lists) { if (lists == null || lists.length == 0) { return null; } if (lists.length == 1) { return lists[0]; } PriorityQueue queue = new PriorityQueue<>(Comparator.comparingInt(n -> n.val)); for (ListNode node : lists) { if (node == null) { continue; } queue.offer(node); } ListNode dummyHead = new ListNode(0); ListNode prev = dummyHead; while (!queue.isEmpty()) { ListNode node = queue.poll(); prev.next = node; prev = node; if (node.next != null) { queue.offer(node.next); node.next = null; } } return dummyHead.next; }
题外话:另一种十分简单的解法是Divide & Conquer:合并两个有序列表(按第一种思路)的时间复杂度是O(n),n是两个列表中的元素总和。可以把k个列表两两分组,并将其合并成一组更长的列表;然后继续两两分组合并,直到最后只剩一个列表为止。这看起来跟体育比赛分组类似,没什么特别的,但观察一下的话,一共有log2(k)轮的分组,每轮都要遍历所有N个元素,因此,时间复杂度也是O(N * log2(k))。
public ListNode mergeKLists(ListNode[] lists) { if (lists == null || lists.length == 0) { return null; } if (lists.length == 1) { return lists[0]; } int i = 0; List mergedList = new ArrayList<>(); // 两两分组合并。merge2()的作用是普通的合并两个有序列表。 while (i < lists.length) { ListNode node1 = lists[i]; if (++i < lists.length) { mergedList.add(merge2(node1, lists[i++])); } else { mergedList.add(node1); } } return mergeKLists(mergedList.toArray(new ListNode[0])); }
Map
HashMap经常用来提高程序的效率。写入(put)、查找(contains)和取值(get)都为O(1)。这里有一些简单写法:
// 计数:记录相同词语出现的次数 Map<String, Integer> map = new HashMap<>(); for (String s : list) { map.merge(s, 1, Integer::sum); // 如果没有,值设为1;否则,在原来的基础上加1 } // map的值为一个列表。如果不存在,创建列表。 // key为字符串,value为储存字符串索引位置的列表 Map<String, List<Integer>> map = new HashMap<>(); for (int i = 0; i < list.size(); ++i) { map.computeIfAbsent(list.get(i), k -> new ArrayList<>()).add(i); }
Bitwise & Bit Shift Operations
// Unary bitwise complement (~) int n = ~5; // ~0101 --> 1010 // Bitwise AND (&) int m = 0b0001; int result = n & m; // 1010 -> 0000 // Bitwise inclusive OR (|) result = n | m; // 1011 // Bitwise exclusive OR (XOR) (^) result = n ^ 0b1111; // 1010 --> 0101 // Signed left shift (<<): shift once results integer x 2 result = 8 << 1; // 16 result = -8 << 1; // -16 // Signed right shift (>>): shift to right and fill 0 on left, but keep leftmost bit. // shift once results integer / 2 result = 8 >> 1; // 4 result = -8 >> 1; // -4 // Unsigned right shift (>>>): shift to right and fill 0 on left. result = 8 >>> 1; // 4 result = -8 >>> 1; // 2147483644
一些常用的用法:
// 乘或除2的n次方 int result = 3 << 4; // 3 * (2 ^ 4) result = 32 >> 3; // 32 / (2 ^ 3) // 判断奇偶 boolean isEven = (123 & 1) == 0; // 注意由于优先级的原因,应使用括号 // 判断某位是否为1(比如每个位表示一种flag) int mask = 0b0010; boolean isSecondBitFromRightSet = (7 & mask) == mask; // 7 = 0b0111, 7 & mask = 0b0010 // XOR: // a XOR a = 0 // a XOR 0 = a // a XOR b XOR c = a XOR (b XOR c) // a XOR b XOR b = a
Trie
Trie又名“前缀树”,用于根据前缀查找词汇。典型的应用包括:自动完成、拼写检查、IP路由、解字谜等等。哈希表虽说查找的时间复杂度为O(1),但在查找相同前缀的词汇时就缺乏效率了。而且如果输入了大量的词汇的话,还有因哈希冲突而导致的效率下降。
如果输入的词汇数量为n,查找的前缀长度为m,那么查找的时间复杂度为O(m)。
这里是一个简单的实现:
public class Trie { public static class Node { private String word; private final Map<Character, Node> childrenMap = new HashMap<>(); public Node() { // empty } public boolean hasChild(char c) { return childrenMap.containsKey(c); } public Node getChild(char c) { return childrenMap.get(c); } public Node addChild(char c) { return childrenMap.computeIfAbsent(c, k -> new Node()); } public void setWord(String str) { this.word = str; } public String getWord() { return word; } } private final Node root = new Node(); public Trie() { // empty } // 输入一个词汇 public void insert(String word) { Node node = root; for (char c : word.toCharArray()) { node = node.addChild(c); } node.setWord(word); } // 查看是否有该词汇 public boolean search(String word) { Node node = root; for (char c : word.toCharArray()) { node = node.getChild(c); if (node == null) { return false; } } return node.getWord() != null; } // 查看是否有以该前缀开始的词汇 public boolean startsWith(String prefix) { Node node = root; for (char c : prefix.toCharArray()) { node = node.getChild(c); if (node == null) { return false; } } return true; } }
如果词汇的组成字符是有限的(比如小写英文单词由26个字母组成),另一种实现方法是用一个数组代替Trie.Node中的HashMap。我个人不太喜欢这种方法。但是用Java语言的时候,讽刺的是这种方法往往能快一点点。
public static class Node { private static final int N = 26; // 字符总数 private Node[] links = new Node[N]; // 查找时用索引 int i = c - 'a'; // 用flag代替上面的字符串能节省点空间。 // 用字符串的目的是在需要得到词汇时不用从头遍历到目前结点,而是能直接拿到 private boolean end = false; // ... private int getIndex(char c) { return c - 'a'; } public boolean contains(char c) { return get(c) != null; } public Node get(char c) { return links[getIndex(c)]; } // ... }