您的位置:首页 > 理论基础 > 数据结构算法

LintCode数据结构题总结

2016-06-10 22:46 507 查看
Lintcode数据结构有这么一些题目:





495. Implement Stack

用任意的数据结构实现一个栈,我是用List实现的,记得写的时候写成List<Integer> array = new ArrayList<Integer>(); 因为List只是Interface,而实现List接口的有多个类,其中一种就是ArrayList,然后记住List的三种操作是add/remove/get,ArrayList可以通过remove操作删除任意下标的元素,可以通过get获得任意下标的元素,可以通过add在末尾添加元素。

40. Implement Queue by Two Stacks

用两个栈stack1、stack2 来实现队列,记得java中自带的Stack是这么写的Stack<Integer>。

push操作:就直接将元素加到stack1上。

pop操作:如果stack2里面有元素,就直接把stack2里的元素pop出来。如果stack2里面没有元素,就把stack1里的所有元素转移到stack2中,然后再把stack2里的元素pop出来。

top操作:类似于pop操作,不再赘述。

12. Min Stack

要求实现一个栈,这个栈除了push和pop操作外,还要有一个min操作,可以随时拿出stack里面的最小值。我是用2个栈实现的,一个栈用于存储基本元素,另一个栈用于存当前的最小值。

A)第一种方案比较耗空间,stack里面有n个元素,那minStack里面也得有n个元素。这样每次push的时候,minStack里都存进一个当前的最小值;每次pop的时候,当前的最小值也同时pop出来了。如下图所示:



B)第二种方案比较省空间,minStack的上层元素比下层元素要小,minStack是一个最小元素在上面的栈。

每次push的时候,只有当新元素 ≤ minStack里最小的元素(即minStack的顶部元素)的时候,我才把新元素也push进minStack。

每次pop的时候,我先pop 主stack的top元素,而对于minStack的top元素,只有当stack里的top元素和minStack的top元素相等时,我才把minStack的top元素也pop掉。如下所示:



记得判断元素相等的时候用equals函数!!!如果在这里直接用==判断的话,他是判断地址而不是判断值

494. Implement Stack by Two Queues

用两个队列来实现栈,记得java中队列得这样声明才行:Queue<Integer> q1 = new LinkedList<Integer>();  Queue是Interface,实现Queue的方式之一是LinkedList

然后java自带的Queue有两种操作:往队列为部加元素是offer(),获得队列头部元素并同时删除队列头部元素的操作是poll()

我们用两个队列q1和q2来实现,其中q1是存元素的主队列,q2是辅助操作队列

实现栈的push操作时,直接往q1里面添加元素就行

实现栈的pop操作时,因为我要pop的是q1的最末尾的元素,所以我就先把q1的前n-1个元素放到q2,然后再把q1唯一的元素给弹出来。然后再把q2的元素放回q1(或者直接交换q1和q2的引用)

实现top操作和pop操作类似。

实现empty操作时,就判断q1和q2的大小是否为0

492. Implement Queue by Linked List

用链表实现一个Queue,要求有如下两个功能:

enqueue(item). Put a new item in the queue. 

dequeue(). Move the first item out of the queue, return it.

实现方法就用头尾指针,进队的时候将尾指针的值设置为新元素,然后尾指针后面添加一个新的尾指针。出队的时候在头结点后面删除一个节点。纯粹考链表基本操作。

class Node {
public int val;
public Node next;
public Node(int n) {
val = n;
next = null;
}
}

public class Queue {
public Node first, last;
public Queue() {
first = new Node(-1);
last = new Node(-1);
first.next = last;
}

public void enqueue(int item) {
Node tmp = new Node(-1);
last.val = item;
last.next = tmp;
last = tmp;
}

public int dequeue() {
int tmp = first.next.val;
first.next = first.next.next;
return tmp;
}
}


493. Implement Queue by Linked List II

多了几个操作,push front和pop back操作。

push_front(item)
. Add a new item to the front of queue.
push_back(item)
. Add a new item to the back of the queue.
pop_front()
. Move the first item out of the queue, return
it.
pop_back()
. Move the last item out of the queue, return
it.

如果用双向链表的话,四个操作都能在O(1)时间复杂度内完成,如果只用单向链表,则pop_back操作要O(n).我是用单向链表完成的:

class Node {
public int val;
public Node next;
public Node(int n) {
val = n;
next = null;
}
}
public class Dequeue {
private Node first, last;
public Dequeue() {
// do initialize if necessary
first = new Node(-1);
last = new Node(-1);
first.next = last;
}

public void push_front(int item) {
Node tmp = new Node(item);
tmp.next = first.next;
first.next = tmp;
}

public void push_back(int item) {
Node tmp = new Node(-1);
last.val = item;
last.next = tmp;
last = tmp;
}

public int pop_front() {
int tmp = first.next.val;
first.next = first.next.next;
return tmp;
}

public int pop_back() {
Node p = first;
while (p.next != last) {
p = p.next;
}
int tmp = p.val;
p.val = -1;
last = p;
return tmp;
}
}

229. Stack Sorting
对一个栈进行排序,要求只能用到一个额外的辅助栈,

| |
|3|
|1|
|2|
|4|
-

排序完后变这样:

| |
|4|
|3|
|2|
|1|
-

我是用一个辅助栈tmp搞定的。

借鉴冒泡排序的算法,我第一趟把原栈中最大的元素放入辅助栈,第二趟把原栈中第二大的元素沉入辅助栈,以此类推。

每趟遍历的目的就是为了找到原栈中当前的最大值。

可设置一个变量mid,用于记录那一趟遍历所找到的“当前”最大值。

在每一趟遍历中,借助那个变量mid找更新原栈中的最大值,同时把所有比mid小的元素都倒腾到辅助栈中。这个时候mid存入了当前的最大值,然后所有其它元素都在辅助栈中,原栈是空的。然后再把辅助栈中的所有比mid小的元素移动回原栈,再把mid移入辅助栈。这样就保持了辅助栈的下面的元素都比上面的大。

记住要用一个遍历count来处理多个元素相同的情况。然后把count数目个mid元素沉入辅助栈。

然后继续处理下一个原栈的栈顶元素,最后直到原栈中的所有元素都被转移到有序栈中!

总而言之,算法的思想就是借鉴了冒泡排序,借用一个辅助栈和一个中间变量,每趟都把栈的最大元素沉入辅助栈的下面。然后注意的一点就是如何处理存在多个相等元素的问题。

Time: O(N^2), Space: O(N)。算法直观的运行效果如下:



代码如下:

public void stackSorting(Stack<Integer> stack) {
// 辅助栈
Stack<Integer> tmp = new Stack<Integer>();

int mid = Integer.MIN_VALUE;
// 循环条件:当整个stack还没有排好序时
while (!stack.empty()) {
int count = 1;
if (!stack.empty()) {
mid = stack.pop();
}
// 把stack掏空,找出当前stack的最大值
while (!stack.empty()) {
if (stack.peek() <= mid) {
tmp.push(stack.pop());
} else {
tmp.push(mid);
mid = stack.pop();
}
}
// 复原stack
while (!tmp.empty() && mid >= tmp.peek()) {
// 处理相等元素的情况
if (tmp.peek().equals(mid)) {
count++;
tmp.pop();
continue;
}
stack.push(tmp.pop());
}
// 更新全局最大值
for (int i = 1; i <= count; i++) {
tmp.push(mid);
}
}

while (!tmp.empty()) {
stack.push(tmp.pop());
}
}

128. Hash Function

要求实现一个hash函数:

hashcode("abcd") = (ascii(a) * 333 + ascii(b) * 332+ ascii(c)
*33 + ascii(d)) % HASH_SIZE 

                              = (97* 333 +
98 * 332 + 99 * 33 +100)
% HASH_SIZE

                              = 3595978 % HASH_SIZE
For key="abcd" and size=100, return 78
这道题主要考察对数字溢出的处理,只有把res声明为long长整型,并且在中途不断取模,才能不溢出。

public int hashCode(char[] key,int HASH_SIZE) {
long ans = 0;
for (int i = 0; i < key.length; i++) {
ans = (ans * 33 + (int)(key[i])) % HASH_SIZE;
}
return (int)ans;
}


129. Rehashing
要求做一个hash,size是原来的两倍。就不断遍历原来的hash数组,如果那个位置上存在元素,则计算那个元素的新index下标,并加入到新的hash数组中去。记得处理一个下标下存的多个元素。

545. Top k Largest Numbers II

Implement a data structure, provide two interfaces:
add(number)
. Add a new number in the data structure.
topk()
. Return the top k largest
numbers in this data structure. k is given when we create the data structure.

这道题可以用最小堆来实现,每次要求输出最大的K个元素,而我在建堆的时候就可以控制这个,Java中的PriorityQueue默认是最小堆。每次add的时候,就看优先队列的队首元素(最小堆当前的最小元素)是否比他大,否则的话,就把他两互换一下。这样每次添加元素的时候就能保证优先队列里面一定是最大的K个元素存在里面。
然后topk()函数就直接输出优先队列里的所有元素即可。

public class Solution {
private int maxSize;
private Queue<Integer> q;

public Solution(int k) {
maxSize = k;
q = new PriorityQueue<Integer>();
}

public void add(int num) {
if (q.size() < maxSize) {
q.offer(num);
return;
}
if (num > q.peek()) {
q.poll();
q.offer(num);
}
}

public List<Integer> topk() {
List<Integer> res = new ArrayList<Integer>();
List<Integer> tmp = new ArrayList<Integer>();

while (!q.isEmpty()) {
tmp.add(q.poll());
}
for (Integer it : tmp) {
q.offer(it);
}

for (int i = tmp.size() - 1; i >= 0; i--) {
res.add(tmp.get(i));
}
return res;
}
};


544. Top k Largest Numbers

给定一个数组,要求找到最大的K个元素。

Given 
[3,10,1000,-99,4,100]
 and k = 
3
.
Return 
[1000,
100, 10]
.

和上一题一样,用优先队列(最小堆)来解决。代码如下:

public int[] topk(int[] nums, int k) {
Queue<Integer> q = new PriorityQueue<Integer>();
for (int i = 0; i < nums.length; i++) {
if (q.size() < k) {
q.offer(nums[i]);
} else {
if (nums[i] > q.peek()) {
q.poll();
q.offer(nums[i]);
}
}
}

int[] res = new int[k];
for (int i = k - 1; i >= 0; i--) {
res[i] = q.poll();
}
return res;
}
时间复杂度是O(nlogk),略优于快排。

4. Ugly Number II

Ugly number is a number that only have factors 
2
,
3
 and 
5
.

Design an algorithm to find the nth ugly number. The first 10 ugly numbers are
1,
2, 3, 4, 5, 6, 8, 9, 10, 12...

If 
n=9
,
return 
10
.
找到第N个丑数,我们可以用优先队列来构建丑数,构建丑数的方法类似艾尼脱色拉筛选法。

一开始的元素里有1,我把1拿出来,乘以2、3、5,得到1、2、3、5,加入到优先队列中。

然后把2拿出来,乘以2、3、5,得到4、6、10,加入到优先队列中,得到1、2、3、4、5、6、10.

再把3拿出来,乘以2、3、5,得到6、9、15,加入到优先队列中,得到1、2、3、4、5、6、6、9、10.这里出现了重复元素,需要用到HashMap来保证每次新加进队列的元素是唯一的。

然后每次都把最小的元素pop出来,这样经过N此操作,就可以得到第N个丑数。时间复杂度为O(nlogn),外层遍历为N遍,然后每次遍历的时候,优先队列的插入花销logN的时间。

public Long nthUglyNumber(int n) {
Queue<Long> q = new PriorityQueue<Long>();
HashMap<Long, Boolean> map = new HashMap<Long, Boolean>();
int[] prime = new int[]{2, 3, 5};
q.offer(Long.valueOf(1));

Long res = Long.valueOf(1);
for (int i = 1; i <= n; i++) {
res = q.poll();
for (int j = 0; j < 3; j++) {
Long tmp = res * prime[j];
if (!map.containsKey(tmp)) {
q.offer(tmp);
map.put(tmp, true);
}
}
}

return res;
}
九章上还有一种O(n)的解法:

public int nthUglyNumber(int n) {
List<Integer> uglys = new ArrayList<Integer>();
uglys.add(1);
int cur = 2;
int p1= 0,p2 = 0,p3 = 0;
int min1, min2, min3;
while (uglys.size() < n) {
while (uglys.get(p1) * 2 < cur)
p1++;
min1 = uglys.get(p1) * 2;

while (uglys.get(p2) * 3 < cur)
p2++;
min2 = uglys.get(p2) * 3;

while (uglys.get(p3) * 5 < cur)
p3++;
min3 = uglys.get(p3) * 5;

int next = min1<min2? min1 : min2;
next = next < min3 ? next : min3;

cur = next + 1;
uglys.add(next);
}

return uglys.get(n-1);
}

486. Merge k Sorted Arrays
给定K个有序数组共N个元素,把他们合并为一个有序数组。

Given 3 sorted arrays:
[
[1, 3, 5, 7],
[2, 4, 6],
[0, 8, 9, 10, 11]
]

return 
[0, 1, 2, 3, 4, 5, 6,
7, 8, 9, 10, 11]
.

我用了一个PriorityQueue,时间复杂度是O(NlogK).

public List<Integer> mergekSortedArrays(int[][] arrays) {
List<Integer> res = new ArrayList<Integer>();
Queue<Integer> heap = new PriorityQueue<Integer>();

if (arrays == null || arrays.length == 0) {
return res;
}

for (int i = 0; i < arrays.length; i++) {
for (int j = 0; j < arrays[i].length; j++) {
heap.offer(arrays[i][j]);
}
}

while (heap.size() > 0) {
res.add(heap.poll());
}
return res;
}

122. Largest Rectangle in Histogram
找到直方图里的最大的长方形

Given n non-negative integers representing the histogram's bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.



Above is a histogram where width of each bar is 1, given height = 
[2,1,5,6,2,3]
.



The largest rectangle is shown in the shaded area, which has area = 
10
 unit.
根据木桶原理,一个木桶能装的水取决于最短的那块木板的高度。同理,一个长方形的面积取决于最短的那个木板的高度。这道题可以借鉴这种思想。就是对于每一个木板,找到一这块木板为高度的长方形的面积,需要找到它的左边界和右边界。比如对于上面的第二个元素1,以它为高的长方形,最左可以走到第一个元素,最右可以走到最后一个元素。然后以第二个元素为高的长方形的面积就是6*1=6。代码如下:

public int largestRectangleArea(int[] height) {
int max = 0;
for (int i = 0; i < height.length; i++) {
int left = i, right = i;
while (left > 0 && height[i]<= height[left - 1]) {
left--;
}
while (right < height.length - 1 && height[i] <= height[right + 1]) {
right++;
}

int tmp = 0;
for (int j = left; j <= right; j++) {
tmp += height[i];
}
max = Math.max(tmp, max);
}
return max;
}

关键的地方在于对于每一个元素,如何快速的找到它左边和它右边第一个比它小的元素。如果能在O(n)时间找到,那整个算法的复杂度就是O(n^2)。如果能在O(1)时间找到,那整个算法的复杂度就是O(n)。

用单调栈可以解决这个问题,使得能在O(1)时间复杂度内找到。

单调栈(递增栈),顾名思义就是说这个栈内的元素是单调递增的。

单调栈用于解决的问题,就是找到左(右)边第一个比他小的、或者左(右)  边第一个比他大的元素是谁。

详情参见这个博客:http://www.cnblogs.com/yuzhangcmu/p/4191981.html

每次比较栈顶存的下标对应的元素与当前元素,如果当前元素大于栈顶,则入栈。否则就进行出栈操作,直到当前元素大于栈顶。

每次入栈的元素的左边那个元素肯定就是第一个比他小的元素(因为比他大的元素都被pop掉了)

而每次要取代栈顶的元素的新值就是第一个比他小的右边的元素。因为一定是比他小才pop出去的。

代码如下:

public int largestRectangleArea(int[] height) {
if (height == null || height.length == 0) {
return 0;
}

Stack<Integer> stack = new Stack<Integer>();
int max = 0;
for (int i = 0; i <= height.length; i++) {
int curt = (i == height.length) ? -1 : height[i];
while (!stack.isEmpty() && curt <= height[stack.peek()]) {
int h = height[stack.pop()];
int w = stack.isEmpty() ? i : i - stack.peek() - 1;
max = Math.max(max, h * w);
}
stack.push(i);
}

return max;
}

时间复杂度是O(n),注意观察,每个点都被push或者pop了一次,所以总共的操作是2n次,所以时间复杂度是O(n)

510. Maximal Rectangle

跟上面那道题类似,只不过高度不再是以一个数字给出,而是以一个二维数组的形式表示,所以时间复杂度多了一个循环,是O(n^2),代码如下:

private int cal(boolean[][] m, int row, int col) {
if (m[row][col] == false) {
return 0;
}
int height = 1;
row--;

while (row >= 0) {
if (m[row][col] == true) {
height++;
row--;
} else {
break;
}
}
return height;
}
public int maximalRectangle(boolean[][] matrix) {
if (matrix == null || matrix.length == 0) {
return 0;
}

int max = 0;
for (int i = 0; i < matrix.length; i++) {
Stack<Integer> stack = new Stack<Integer>();
for (int j = 0; j <= matrix[i].length; j++) {
int cur = (j == matrix[i].length) ? -1 : cal(matrix, i, j);
while (!stack.isEmpty() && cur <= cal(matrix, i, stack.peek())) {
int h = cal(matrix, i, stack.pop());
int w = stack.isEmpty() ? j : j - 1 - stack.peek();
max = Math.max(max, h * w);
}
stack.push(j);
}
}

return max;
}

126. Max Tree
Given an integer array with no duplicates. A max tree building on this array is defined as follow:

The root is the maximum number in the array
The left subtree and right subtree are the max trees of the subarray divided by the root number.

Construct the max tree by the given array.

Given 
[2, 5, 6, 0, 3, 1]
, the max tree constructed by this array is:
6
/ \
5   3
/   / \
2   0   1

注意题目的一个重要条件就是左子树和右子树元素分别是被父节点元素切分开的子数组中的最大值。根据这个题目条件可以判断出给出的数组是Max Tree的中序遍历的结果。而中序遍历的访问顺序是这样子的:左子树 根节点 右子树,如下图所示:



以上图来说,假设我们想找一个node(右1)的父节点:

根据max tree的性质,node(右1)的父节点需要比他大,但是不能太大。(如果太大就可以做他的父节点的父节点了,所谓爷爷节点)

结合中序遍历的性质,他的根节点肯定要么是它左边第一个,要么就是它右边第一个。

综上,结合中序遍历以及max tree本身的性质来看,要找到一个元素的父节点,那这个父节点 = Math.min(左边第一个比它大的,右边第一个比它大的) 。

这道题我一开始使用递归的方法构建的,在数组中找到最大的元素作为root,然后最大元素的左数组区间中找到最大值最为root的左节点,在右数组区间中找到的最大值为root的右节点,递归构造左右子树。代码如下:

private int maxIndex(int[] A, int left, int right) {
int maxIndex = -1;
int max = Integer.MIN_VALUE;
for (int i = left; i <= right; i++) {
if (A[i] >= max) {
maxIndex = i;
max = A[i];
}
}
return maxIndex;
}
private TreeNode buildTree(int[] A, int l, int r) {
if (l > r) {
return null;
}
if (l == r) {
return new TreeNode(A[l]);
}

int index = maxIndex(A, l, r);
TreeNode root = new TreeNode(A[index]);

root.left = buildTree(A, l, index - 1);
root.right = buildTree(A, index + 1, r);

return root;
}
public TreeNode maxTree(int[] A) {
TreeNode root = buildTree(A, 0, A.length - 1);
return root;
}
但是这个并没能通过极端测例,那就是当输入是[999999,999998,999997,```3,2,1]时,算法会出现极端情况,时间复杂度变为O(n^2),导致stackoverflow error,爆栈了。

解答:如果能够确定每个节点的父亲节点,则可以构造出整棵树。找出每个数往左数第一个比他大的数和往右数第一个比他大的数,两者中较小的数即为该数的父亲节点。如:[3,1,2],3没有父亲节点,1的父亲节点为2,2的父亲节为3。并且可以根据与父亲的位置关系来确定是左儿子还是右儿子。接下来的问题是如何快速找出每个数往左、往右第一个比他大的数。这里需要用到数据结构栈。以找每个数左边第一个比他大的数为例,从左到右遍历每个数,栈中保持递减序列,新来的数不停的Pop出栈顶直到栈顶比新数大或没有数。以[3,1,2]为例,首先3入栈,接下来1比3小,无需pop出3,1入栈,并且确定了1往左第一个比他大的数为3。接下来2比1大,1出栈,2比3小,2入栈。并且确定了2往左第一个比他大的数为3。用同样的方法可以求得每个数往右第一个比他大的数。时间复杂度O(n),空间复杂度也是O(n)为最优解法。

原题

给出一个没有重复的整数数组,在此数组上建立最大树的定义如下:

1.根是数组中最大的数

2.左子树和右子树元素分别是被父节点元素切分开的子数组中的最大值

利用给定的数组构造最大树。

给出数组 [2, 5, 6, 0, 3, 1],构造的最大树如下:

     6
/ \
5   3
/   / \
2   0   1


通过观察发现规律,每个node的父亲节点 = Math.min(左边第一个比它大的,右边第一个比它大的) 

维护一个降序数组,可以实现对这个min的快速查找

# stack = [2], push 5 因为 5 > 2, 所以2是5的左儿子, pop 2
# stack = [], push 5
# stack = [5], push 6, 因为 6 > 5,所以5是6的左儿子, pop 5
# stack = [], push 6
# stack = [6], push 0, 因为0 < 6, stack = [6], 所以0是6的右儿子
# stack = [6, 0], push 3, 因为3 > 0, 所以0是3的左儿子, pop 0
# stack = [6], 所以3是6的右儿子, push 3
# stack = [6, 3], push 1, 因为1 < 3,所以1是3的右儿子

所以能AC的代码如下,时间复杂度是O(n):

public TreeNode maxTree(int[] A) {
TreeNode[] stack = new TreeNode[A.length];
for (int i = 0; i < A.length; ++i) {
stack[i] = new TreeNode(0);
}

int cnt = 0;
for (int i = 0; i < A.length; i++) {
TreeNode tmp = new TreeNode(A[i]);

while (cnt > 0 && A[i] > stack[cnt - 1].val) {
tmp.left = stack[cnt - 1];
cnt--;
}
if (cnt > 0) {
stack[cnt - 1].right = tmp;
}
stack[cnt++] = tmp;
}
return stack[0];
}


面试官角度:首先容易想到的是使用递归的方法来构造MaxTree,每一层递归用O(n)的时间找到最大数,然后将数组分为左右两个部分,然后递归完成构造。这种算法在极端情况下复杂度可能达到O(n^2),所以并不能被面试官所接受。但是你首先至少要把这种暴力的方法答出,并分析出最坏时间复杂度,因为这至少也体现出了你一部分的算法能力和时间复杂度分析的技巧。万一后面的正确方法答不出来,至少不会是0分。最优算法所使用到的Stack的方法,是一个非常常用的解题技巧。我们在今后的面试题中也会陆续为为大家讲解涉及到这种方法的题目。




124. Longest Consecutive Sequence

给你一个n个数的乱序序列,O(N)找出其中最长的连续序列的长度。 例如给你[100, 4, 200, 1, 3, 2],那么最长的连续序列为[1, 2, 3, 4],所以返回4。 

最简单直接的想法是:将数组排序,然后扫一遍排好序的序列,从中找出最长的即可,这样的话时间是O(nlogn)+O(n),显然不符合题目要求,会超时。

这道题得用到HashSet,因为HashSet的查找操作是O(1)。对于序列里任意一个数A[i],我们可以通过set马上能知道A[i]+1和A[i]-1是否也在序列中。如果在,继续找A[i]+2和A[i]-2,以此类推,直到将整个连续序列找到。为了避免在扫描到A[i]-1时再次重复搜索该序列,在从每次搜索的同时将搜索到的数从set中删除。直到set中为空时,所有连续序列搜索结束。

public int longestConsecutive(int[] num) {
int max = 0;
HashSet<Integer> hash = new HashSet<Integer>();
for (int i = 0; i < num.length; i++) {
hash.add(num[i]);
}
for (int i = 0; i < num.length; i++) {
int down = num[i] - 1;
int up = num[i] + 1;
while (hash.contains(down)) {
hash.remove(down);
down--;
}
while (hash.contains(up)) {
hash.remove(up);
up++;
}
max = Math.max(max, up - down - 1);
}
return max;
}


130. Heapify

要求对一个无序数组简历最小堆。由于最小堆也是一个完全二叉树,所以用数组表示的话,下标为i的元素的左子树是2i+1、右子树是2i+2。

堆化操作的算法,就是从最后一层非叶子节点开始,一下图为例,就是首先从6,然后进行到5,然后进行到根节点8。

然后每次操作的内容就是把当前的根节点和左右子节点中的最小值互换。比如6就要和2互换、5要和1互换。

所以第一趟就是把非叶结点最后一层的第一个做一下heap操作,也就是把小的元素浮上来,大的元素沉下去。

第二趟就是把非叶结点最后一层的第2个node做一下heap操作。

第三趟是把非叶结点倒数第二层的node做一下heap操作,也就是根节点8。但是做了一下操作还不够,因为调换后破坏了根节点的右子树的平衡性。所以我得对根节点的右子树继续进行heap操作。



整个算法的流程跑下来的时间复杂度是O(n)的。

private void minHeap(int[] A, int index) {
int l = index * 2 + 1;
int r = index * 2 + 2;
int small = index;
if (l < A.length && A[l] < A[small]) {
small = l;
}
if (r < A.length && A[r] < A[small]) {
small = r;
}
if (index != small) {
int tmp = A[small];
A[small] = A[index];
A[index] = tmp;
minHeap(A, small);
}

}
public void heapify(int[] A) {
for (int i = A.length / 2; i >= 0; i--) {
minHeap(A, i);
}
}

471. Top K Frequent Words
给一个单词列表,求出这个列表中出现频次最高的K个单词。

你需要按照单词的词频排序后输出,越高频的词排在越前面。如果两个单词出现的次数相同,则词典序小的排在前面。

给出单词列表:
[
"yes", "lint", "code",
"yes", "code", "baby",
"you", "baby", "chrome",
"safari", "lint", "code",
"body", "lint", "code"
]


如果 k = 
3
, 返回 
["code",
"lint", "baby"]


如果 k = 
4
, 返回 
["code",
"lint", "baby", "yes"]


O(nlgk) solution with O(n) space

we could use a Min Heap of size k to keep top k most frequent words. That’s right. We also need to use a hashmap to keep frequency of each word.

Calculated frequency of all the words in a hashmap from the word to its frequency.

Start adding pair object of word and its frequency into a min heap where we use the frequency as the key for the min heap.

If the heap is full then remove the minimum element (top) form the heap and add add the new word-frequency pair only if the frequency of this word has frequency greater than the top word in the heap.

Once we scanned all the words in the map and the heap is properly updated then the elements contained in the min heap are the top k most frequents.

class Pair {
String key;
int value;
Pair(String k, int v) {
this.key = k;
this.value = v;
}
}
public class Solution {
/**
* @param words an array of string
* @param k an integer
* @return an array of string
*/
private Comparator<Pair> comparator = new Comparator<Pair>() {
public int compare(Pair left, Pair right) {
if (left.value != right.value) {
return left.value - right.value;
} else {
return right.key.compareTo(left.key);
}
}
};

public String[] topKFrequentWords(String[] words, int k) {
if (k == 0) {
return new String[0];
}

// Calculate frequency of each word
HashMap<String, Integer> map = new HashMap<String, Integer>();
for (String word : words) {
if (map.containsKey(word) == false) {
map.put(word, 1);
} else {
map.put(word, map.get(word) + 1);
}
}

// Use a PriorityQueue to keep top K words
PriorityQueue<Pair> q = new PriorityQueue<Pair>(k, comparator);
for (String word : map.keySet()) {
Pair newPair = new Pair(word, map.get(word));
if (q.size() < k) {
q.offer(newPair);
} else {
if (comparator.compare(newPair, q.peek()) > 0) {
q.poll();
q.offer(newPair);
}
}
}

String[] res = new String[k];
for (int i = k - 1; i >= 0; i--) {
res[i] = q.poll().key;
}

return res;
}
}


如果要更优的算法,看这个博客:http://www.zrzahid.com/top-k-or-k-most-frequent-words-in-a-document/

230. Animal Shelter

要求实现一个数据结构:

An animal shelter holds only 
dogs
 and 
cats
,
and operates on a strictly
"first in, first out"
 basis.
People must adopt either the 
"oldest"
(based
on arrival time) of all animals at the shelter, or they can select whether they would prefer a dog or a cat (and will receive the oldest animal of that type). They cannot select which specific animal they would like. Create the data structures to maintain
this system and implement operations such as 
enqueue
dequeueAny
dequeueDog
 and 
dequeueCat
.

int CAT = 0
int DOG = 1

enqueue("james", DOG);
enqueue("tom", DOG);
enqueue("mimi", CAT);
dequeueAny();  // should return "james"
dequeueCat();  // should return "mimi"
dequeueDog();  // should return "tom"

这个题目的基本思路就是要实现一个先进先出的队列,但是相较于平时我们使用的queue,它多的一个不同之处就是要求对指定的类型进行先进先出。所以需要用一个新的类型来作为存储类型。然后每次出队列的时候,就逐个遍历队列中的元素,并不断出队入队。

class Node {
String name;
int type;
Node(String n, int t) {
this.name = n;
this.type = t;
}
}
public class AnimalShelter {

private Queue<Node> q;
public AnimalShelter() {
q = new LinkedList<Node>();
}

/**
* @param name a string
* @param type an integer, 1 if Animal is dog or 0
* @return void
*/
void enqueue(String name, int type) {
q.offer(new Node(name, type));
}

public String dequeueAny() {
Node tmp = q.poll();
return tmp.name;
}

public String dequeueDog() {
return dequeueHelper(1);
}

public String dequeueCat() {
return dequeueHelper(0);
}

private String dequeueHelper(int type) {
int stepsMoved = 0;
while (q.peek().type != type) {
q.offer(q.poll());
stepsMoved++;
}
Node res = q.poll();
int stepsLeft = q.size() - stepsMoved;
for (int i = 0; i < stepsLeft; i++) {
q.offer(q.poll());
}
return res.name;
}
}


24. LFU Cache

这道题有点类似于LRU Cache,也算是一道数据结构的系统设计题。要求如下:

LFU (Least Frequently Used) is a famous cache eviction algorithm.

For a cache with capacity k, if the cache is full and need to evict a key in it, the key with the lease frequently used will be kicked out.

Implement 
set
 and 
get
 method
for LFU cache.

Example

Given 
capacity=3

set(2,2)
set(1,1)
get(2)
>> 2
get(1)
>> 1
get(2)
>> 2
set(3,3)
set(4,4)
get(3)
>> -1
get(2)
>> 2
get(1)
>> 1
get(4)
>> 4


与LRU不同,LFU关注的是频率。一个元素的频率只有当它还在cache中则记录,若一个元素被evict后,那他的频率就清零了。关于LFU Cache有篇著名的paper:http://dhruvbird.com/lfu.pdf  而我在这里的算法基本也是基于它的。

我们需要以下几个东西:

1)HashMap,用于快速的获取 (get)、存储 (set)元素。

2)一个新的数据结构Node,用于存储3种类型的信息:key、value、frequency

3)一个LinkedHashSet数组,数组的每个元素是一个集合Set,而数组的下标就代表着frequency频率,比如下标2就代表那个位置的集合Set存的是出现过2次的元素。

至于为啥要用LinkedHashSet呢,因为如果假如缓存容量达到了上限,而A元素和B元素都是频率最低的,并且它俩出现的频率都是1,那么我该射出哪一个元素呢?根据题目的意思,应该是射出最先insert进来的元素。假如一开始是A先进来,那就排出A。根据Java API,LinkedHashSet有这种性质: This
linked list defines the iteration ordering, which is the order in which elements were inserted into the set (insertion-order).
Note that insertion order is not affected
if an element is re-inserted into
the set.

也就是重新插入后,原顺序不改变,所以我们就要使用LinkedHashSet。

带图片的操作可以看下这个博客:http://javarticles.com/2012/06/lfu-cache.html

对于get函数,我们每次get的时候首先从hashmap中通过key来获取那个元素,假如那个元素不存在,则返回-1;如果存在,则返回它的value值,并同时在数组中找到那个元素给它的频率+1。

对于set函数,同样也是在hashmap中通过key来获取那个元素,如果元素存在,则更新它的value值;若不存在,则先看cache有没有到达上限,如果到达上限了,我们就要首先射出一个频率最低的元素 (evict)。然后就通过这个key和value新建一个Node节点,添加进数组的第一个集合里(对应的数组下标为0),同时添加进hashmap中。然后给那个元素的频率+1。

<pre name="code" class="java">public class LFUCache {
private class Node {
public int key;
public int value;
public int frequency;
public Node (int k, int v, int f) {
this.key = k;
this.value = v;
this.frequency = f;
}
}
private HashMap<Integer, Node> map;
private LinkedHashSet<Node>[] list;
private int maxFrequency;
private int maxMapSize;

public LFUCache(int capacity) {
map = new HashMap<Integer, Node>(capacity);
list = new LinkedHashSet[capacity * 2];
maxFrequency = capacity * 2 - 1;
maxMapSize = capacity;
for (int i = 0; i <= maxFrequency; i++) {
list[i] = new LinkedHashSet<Node>();
}
}

public void set(int key, int value) {
Node node = map.get(key);
if (node != null) {
node.value = value;
} else {
if (map.size() == maxMapSize) {
evict();
}
node = new Node(key, value, 0);
LinkedHashSet<Node> set = list[0];
set.add(node);
map.put(key, node);
}
addFrequency(node);
}

public int get(int key) {
Node node = map.get(key);
if (node == null) {
return -1;
} else {
addFrequency(node);
return node.value;
}
}

public void addFrequency(Node node) {
int freq = node.frequency;
if (freq < maxFrequency) {
LinkedHashSet<Node> set = list[freq];
LinkedHashSet<Node> nextSet = list[freq + 1];
// move to next frequency position
set.remove(node);
nextSet.add(node);
node.frequency = freq + 1;
map.put(node.key, node);
} else {
// Hybrid with LRU: put most recently accessed ahead of others:
LinkedHashSet<Node> set = list[freq];
set.remove(node);
set.add(node);
}
}

private void evict() {
for (int i = 0; i < list.length; i++) {
if (!list[i].isEmpty()) {
LinkedHashSet<Node> set = list[i];
Iterator<Node> it = set.iterator();
Node node = it.next();
it.remove();
map.remove(node.key);
break;
}
}
}
}


423. Valid Parentheses

判断一连串的括号是不是合法的,用到的数据结构是栈stack。普林斯顿的算法课讲过这个题目。有大中小三种括号,

就用一个栈就行,栈为空的时候,就入栈。

若栈不为空,并且出现了括号匹配,则把栈顶元素弹出。若没有出现括号匹配,则继续入栈,直到遍历到最后一个括号。

如果这些括号都是合法的,那么到最后的栈应该是空的。

private boolean match(char a, char b) {
if (a == '{' && b == '}' || a == '[' && b == ']' || a == '(' && b == ')') {
return true;
}
return false;
}
public boolean isValidParentheses(String s) {
Stack<Character> stack = new Stack();
for (Character c : s.toCharArray()) {
if (stack.empty()) {
stack.push(c);
} else {
if (match(stack.peek(), c)) {
stack.pop();
} else {
stack.push(c);
}
}
}
return stack.empty();
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息