大纲 cs3k.com
- 数据结构概述
- 哈希表 Hash: a.原理 b.应用
- 堆 Heap: a.原理 b.应用-优先队列 Priority Queue c.替代品-TreeMap
数据结构的两类问题
cs3k.com
1.设计一个数据结构
2.实现某个算法用到了某个/某几个数据结构
什么是数据结构
可以认为是一个集合,并且提供集合上的若干操作
LINEAR DATA STRUCTURE,通常用数组实现
-Queue
-Stack
-Hash
TREE DATA STRUCTURE,通常用指针
-Tree
QUEUE : BFS
O(1) Push Pop Top
STACK : DFS
O(1) Push Pop Top
题外话
算法要具象化,数据结构也要具象化
栈好像一个大箱子,往里面一本本放书,拿的时候得从最上面的拿。
queue就是排队,从后面进,从前面出。
Queue的实现
用哪种底层的数据结构实现Queue呢?
- 用linked list的实现非常直观
- 循环数组和动态数组:
2.1 循环数组1 2 3 4…10十个坑, 1不用了, 把1 删了,然后加11进去, 11占得是1的坑, 每个坑可以循环利用.
2.2 动态数组就是c++里的vector java 里的array list
开一百个坑, 用满了
然后开2*100个, 把前100个copy过去, 再把前100个删掉.
Hash
cs3k.com
时间复杂度
O(key_size) Insert / O(key_size) Find / O(key_size) Delete
比如key一个整数, 四个字节
实际的插入, 查找, 删除的时间复杂度是O(4)
hash table VS hash map VS hash set
- hash set只有key 没有value, 去重的时候用
- hash table支持线程安全,可以多个线程同事调用一个hash table, 不会出问题
- hash map不支持线程安全, 多个线程一起搞一个hash map会搞砸ps: 因为加锁和解锁很慢, 所以hash table会性能低一些
hash function/ hash code
使命: 对于任意的key,得到一个固定且无规律的介于0~capacity-1的整数
理解: hash map可以理解为一个大数组, hash function 就是找到这个数组的index, 然后把一对存进去
著名的hash 算法
cs3k.com
MD5 SHA-1 SHA2 太复杂, 加密用的,此外
- char->255 整数
- 最简单的是取模,比如key%31转换为31进制, 31为经验值
• 边乘边取模, 以防越界
• java和c++都会自动把越界的减掉
一般hash function是针对string即char的,因为其它的数据形式都可以转化成char
比如int是4byte的,就是4个char
double是8byte的,就是8个char
如果一个class是{2int加上1double}就可以等同一个8+8的string
貌似好像java下面是每个字节×33+字节对的整数取模,其实也就是转换成33进制,再取模
hash function的设计要求是:越乱越好,越没有规律越好
但是如果有一列数101,201,301,401。。那就坑爹了。。。
hash function的collision
cs3k.com
- open hash table:有collision就存个linked list,拉链法类似于上厕所的时候, 看上了一个坑, 就等它, 就在后面排着如果要查找的时候, 就从排着的地方for一遍, 看有没有
- close hash table:有collision就占下一个坑,占坑法类似于上厕所占坑, 坑占了就找下一个, 你占我, 我就占别人的
其中需要注意的是close hash,在删除一个key之后,要标注可用,而不是空位,具体如下:
加入7,3,12三个数字到一个hash function为%5的table里,假设前面一小部分如下:
其中7加到index为2的,3加到index为3的,到12的时候,算出来的index是2,但是2已经被占了,所以向后挪一个,去看看3,结果3也被占了,所以12就被塞到了index为4的地方
当删除3的时候,不能直接把index为3的位置直接标空位,而应该标available,这样查询12的时候,会去2找,没有去看3,发现available,知道之前被占过,然后接着向后找
rehashing
open hashing 和close hashing 都要rehashing
空间大VS空间小
空间大 非空间
空间小 查找时间长
所以trade off一下
Rehashing
The size of the hash table is not determinate at the very beginning. If the total size of keys is too large (e.g. size >= capacity / 10), we should double the size of the hash table and rehash every keys.
public class Solution {
/** * @param hashTable: A list of The first node of linked list * @return: A list of The first node of linked list which have twice size */ public ListNode[] rehashing(ListNode[] hashTable) { // write your code here if (hashTable.length <= 0) { return hashTable; } int newcapacity = 2 * hashTable.length; ListNode[] newTable = new ListNode[newcapacity]; for (int i = 0; i < hashTable.length; i++) { while (hashTable[i] != null) { int newindex = (hashTable[i].val % newcapacity + newcapacity) % newcapacity; if (newTable[newindex] == null) { newTable[newindex] = new ListNode(hashTable[i].val); // newTable[newindex].next = null; } else { ListNode dummy = newTable[newindex]; while (dummy.next != null) { dummy = dummy.next; } dummy.next = new ListNode(hashTable[i].val); } hashTable[i] = hashTable[i].next; } } return newTable; } }
标准:占了超过10%就要rehashing
size是实际被占的,如果实际被占的空间超过十分之一,冲突率太高。如果数组需要开的更大,就需要开一个更大的数组,并且把原来的小的copy过去,类似于动态数组,但是很多时候hash function就会变,所以最好不要轻易折腾,举个栗子:
本来有[4,1,2,3]四个数,其中他们的位置按照%4得到的
我们要扩充数组到八个坑,我们开八个坑,要把1,2,3,4挪过去。但是这次1,2,3,4要根据%8来找他们的位置,而不是直接copy过去,所以增加了不少计算量
哈希重建
因为哈希表只膨胀, 不收缩, 所以对于是不是加一个又删一个的操作, 就要偶尔destroy了再重建一个
LRU cache 和LFU cache
cs3k.com
cache的原理就是比较hot的条目放速度快的地方存着(内存),不hot的放速度慢的(硬盘),评价hot与否的原则是:
LRU: last recent used 时间戳, 坑不够, 淘汰最老的(此外还有LFU: last frequent used,不要求掌握)
假设一个LRU cache只有三个坑,最近的是
2->1->3
我们现在出现一个新的使用是2 我们要变成
1->3->2
出现了5,变成
3->2->5
LRU中,因为有冲突,所以需要链表, 而给一个key, 需要的知道链表在哪儿
所以实现方法为 linked list+ hashmap
LRU Cache
Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and set.
get(key) – Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
set(key, value) – Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.
这个其中set和get都是visit
- 用doubly linked list实现:
在2往后挪的时候, hash不变, 但是1和3受影响, 所以可用doubly linked list
- 用singly linked list实现:
每个key对应的value的值是prev的点
2挪到尾巴, 1.next = 1.next.next就行了
public class LRUCache {
private class Node{ Node prev; Node next; int key; int value; public Node(int key, int value) { this.key = key; this.value = value; this.prev = null; this.next = null; } } private int capacity; private HashMap hs = new HashMap(); private Node head = new Node(-1, -1); private Node tail = new Node(-1, -1); public LRUCache(int capacity) { this.capacity = capacity; tail.prev = head; head.next = tail; } public int get(int key) { if( !hs.containsKey(key)) { return -1; } // remove current Node current = hs.get(key); current.prev.next = current.next; current.next.prev = current.prev; // move current to tail move_to_tail(current); return hs.get(key).value; } public void set(int key, int value) { if( get(key) != -1) { hs.get(key).value = value; return; } if (hs.size() == capacity) { hs.remove(head.next.key); head.next = head.next.next; head.next.prev = head; } Node insert = new Node(key, value); hs.put(key, insert); move_to_tail(insert); } private void move_to_tail(Node current) { current.prev = tail.prev; tail.prev = current; current.prev.next = current; current.next = tail; } }
heap
O(log N) Add / O(log N) Remove / O(1) Min or Max
用于设计最大最小值的问题
priority queue是一个阉割版的heap, 叫做queue,其实是heap(只实现了部分heap的功能), 每次优先级最高的出列
只能add一个和remove一个, 删除是O(n)
算法时间复杂度相关
longest palindrome substring标准算法是叫manche algorithm O(n), 另有基于它的O(nlogn)算法, 但是面试写出O(n^2)就可以
Ugly Number
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…
// version 1: O(n) scan
class Solution {
/**
* @param n an integer
* @return the nth prime number as description.
*/
public int nthUglyNumber(int n) { List uglys = new ArrayList(); uglys.add(1); int p2 = 0, p3 = 0, p5 = 0; // p2, p3 & p5 share the same queue: uglys for (int i = 1; i < n; i++) { int lastNumber = uglys.get(i - 1); while (uglys.get(p2) * 2 <= lastNumber) p2++; while (uglys.get(p3) * 3 <= lastNumber) p3++; while (uglys.get(p5) * 5 <= lastNumber) p5++; uglys.add(Math.min( Math.min(uglys.get(p2) * 2, uglys.get(p3) * 3), uglys.get(p5) * 5 )); } return uglys.get(n - 1); } }; // version 2 O(nlogn) HashMap + Heap class Solution { /** * @param n an integer * @return the nth prime number as description. */ public int nthUglyNumber(int n) { // Write your code here Queue Q = new PriorityQueue(); HashSet inQ = new HashSet(); Long[] primes = new Long[3]; primes[0] = Long.valueOf(2); primes[1] = Long.valueOf(3); primes[2] = Long.valueOf(5); for (int i = 0; i < 3; i++) { Q.add(primes[i]); inQ.add(primes[i]); } Long number = Long.valueOf(1); for (int i = 1; i < n; i++) { number = Q.poll(); for (int j = 0; j < 3; j++) { if (!inQ.contains(primes[j] * number)) { Q.add(number * primes[j]); inQ.add(number * primes[j]); } } } return number.intValue(); } };
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.
top之间比最弱, 用min heap.
add O(logk)
topk O(klogk)
但是kth largest number 用quick select O(n)
什么时候用QUICK SELECT 什么时候用HEAP呢
heap是Nlogk, 时长要知道前k个是谁, 是流动的活数据
而quick sort是O(N), 找从小到大第k个, 是离线的死数据, 一次行的
public class Solution {
private int maxSize; private Queue minheap; public Solution(int k) { minheap = new PriorityQueue(); maxSize = k; } public void add(int num) { if (minheap.size() < maxSize) { minheap.offer(num); return; } if (num > minheap.peek()) { minheap.poll(); minheap.offer(num); } } public List topk() { Iterator it = minheap.iterator(); List result = new ArrayList(); while (it.hasNext()) { result.add((Integer) it.next()); } Collections.sort(result, Collections.reverseOrder()); return result; } };
Merge k Sorted Lists
cs3k.com
Merge k sorted linked lists and return it as one sorted list.
类似的题有external sorting
我只有1G内存,但是要排序4G的数组
就分4个1G的分别排好, 再合并
1. k路归并算法, 用heap
经典实现用heap,时间O(Nlogk), 谁小谁出列, k个数找最小, 用heap
2. 用priority queue 的实现
重点是priority queue的comparator的实现
从小到大是第一个参数a减第二个参数b
从大到小是第二个参数减第一个参数
第一个参数减第二个参数为什么是从小到大呢?首先我们看定义
Syntax:
In their implementation in the C++ Standard Template Library, priority queues take three template parameters:1
2 template < class T, class Container = vector<T>,
class Compare = less<typename Container::value_type> > class priority_queue;
Where the template parameters have the following meanings:
T: Type of the elements.
Container: Type of the underlying container object used to store and access the elements.
Compare: Comparison class: A class such that the expression comp(a,b), where comp is an object of this class and a and b are elements of the container, returns true if a is to be placed earlier than b in a strict weak ordering operation. This can either be a class implementing a function call operator or a pointer to a function. This defaults to less<T>, which returns the same as applying the less-than operator (a<b).
The priority_queue object uses this expression when an element is inserted or removed from it (using push or pop, respectively) to grant that the element popped is always the greater in the priority queue.
参考定义呢,comparator为真的时候,就是a-b>0, a优先级高,先出列,a然后b,这不是由大到小么?反了啊。。。此处如有知道为什么,请指教
答案找到了,可以参考:,以下为转载:
首先函数在头文件<queue>中,归属于命名空间std,使用的时候需要注意。
队列有两种常用的声明方式:
std::priority_queue<T> pq;
std::priority_queue<T, std::vector<T>, cmp> pq;
第一种实现方式较为常用,接下来我给出STL中的对应声明,再加以解释。
template<class _Ty,
class _Container = vector<_Ty>,
class _Pr = less<typename _Container::value_type> >
class priority_queue
大家可以看到,默认模板有三个参数,第一个是优先队列处理的类,第二个参数比较有特点,是容纳优先队列的容器。实际上,优先队列是由这个容器+C语言中关于heap的相关操作实现的。这个容器默认是vector,也可以是dequeue,因为后者功能更强大,而性能相对于vector较差,考虑到包装在优先队列后,后者功能并不能很好发挥,所以一般选择vector来做这个容器。第三个参数比较重要,支持一个比较结构,默认是less,默认情况下,会选择第一个参数决定的类的<运算符来做这个比较函数。
接下来开始坑爹了,虽然用的是less结构,然而,队列的出队顺序却是greater的先出!就是说,这里这个参数其实很傲娇,表示的意思是如果!cmp,则先出列,不管这样实现的目的是啥,大家只能接受这个实现。实际上,这里的第三个参数可以更换成greater,像下面这样:
std::priority_queue<T, std::vector<T>, greater<T>> pq;
一般大家如果是自定义类就干脆重载<号时注意下方向了,没人在这里麻烦,这个选择基本上是在使用int类还想小值先出列时。
从上面的剖析我们也就知道了,想要让自定义类能够使用优先队列,我们要重载小于号。
class Student
{
int id;
char name[20];
bool gender;
bool operator < (Student &a) const
{
return id > a.id;
}
};
就拿这个例子说,我们想让id小的先出列,怎么办,就要很违和的给这个小于符号重载成实际上是大于的定义。
如果我们不使用自定义类,又要用非默认方法去排序怎么办?就比如说在Dijkstra中,我们当然不会用点的序号去排列,无论是正序还是反序,我们想用点到起点的距离这个值来进行排序,我们怎样做呢?细心的读者在阅读我的有关Dijkstra那篇文章时应该就发现了做法——自定义比较结构。优先队列默认使用的是小于结构,而上文的做法是为我们的自定义类去定义新的小于结构来符合优先队列,我们当然也可以自定义比较结构。自定义方法以及使用如下,我直接用Dijkstra那篇的代码来说明:
int cost[MAX_V][MAX_V];
int d[MAX_V], V, s;
//自定义优先队列less比较函数
struct cmp
{
bool operator()(int &a, int &b) const
{
//因为优先出列判定为!cmp,所以反向定义实现最小值优先
return d[a] > d[b];
}
};
void Dijkstra()
{
std::priority_queue<int, std::vector<int>, cmp> pq;
pq.push(s);
d[s] = 0;
while (!pq.empty())
{
int tmp = pq.top();pq.pop();
for (int i = 0;i < V;++i)
{
if (d[i] > d[tmp] + cost[tmp][i])
{
d[i] = d[tmp] + cost[tmp][i];
pq.push(i);
}
}
}
}
同时推荐,是stack,queue和priority_queue的c++操作集合
public class Solution {
private Comparator ListNodeComparator = new Comparator() { public int compare(ListNode left, ListNode right) { return left.val - right.val; } }; public ListNode mergeKLists(List lists) { if (lists == null || lists.size() == 0) { return null; } Queue heap = new PriorityQueue(lists.size(), ListNodeComparator); for (int i = 0; i < lists.size(); i++) { if (lists.get(i) != null) { heap.add(lists.get(i)); } } ListNode dummy = new ListNode(0); ListNode tail = dummy; while (!heap.isEmpty()) { ListNode head = heap.poll(); tail.next = head; tail = head; if (head.next != null) { heap.add(head.next); } } return dummy.next; } }
3.分治法
k个的时候劈一半
一半1~k/2 一半2/k+1~k
每层用时间O(N), 一共logk层
public class Solution {
/** * @param lists: a list of ListNode * @return: The head of one sorted list. */ public ListNode mergeKLists(List lists) { if (lists.size() == 0) { return null; } return mergeHelper(lists, 0, lists.size() - 1); } private ListNode mergeHelper(List lists, int start, int end) { if (start == end) { return lists.get(start); } int mid = start + (end - start) / 2; ListNode left = mergeHelper(lists, start, mid); ListNode right = mergeHelper(lists, mid + 1, end); return mergeTwoLists(left, right); } private ListNode mergeTwoLists(ListNode list1, ListNode list2) { ListNode dummy = new ListNode(0); ListNode tail = dummy; while (list1 != null && list2 != null) { if (list1.val < list2.val) { tail.next = list1; tail = list1; list1 = list1.next; } else { tail.next = list2; tail = list2; list2 = list2.next; } } if (list1 != null) { tail.next = list1; } else { tail.next = list2; } return dummy.next; } }
4.两两归并
本质和上面一个差不多
public class Solution {
/** * @param lists: a list of ListNode * @return: The head of one sorted list. */ public ListNode mergeKLists(List lists) { if (lists == null || lists.size() == 0) { return null; } while (lists.size() > 1) { List new_lists = new ArrayList(); for (int i = 0; i + 1 < lists.size(); i += 2) { ListNode merged_list = merge(lists.get(i), lists.get(i+1)); new_lists.add(merged_list); } if (lists.size() % 2 == 1) { new_lists.add(lists.get(lists.size() - 1)); } lists = new_lists; } return lists.get(0); } private ListNode merge(ListNode a, ListNode b) { ListNode dummy = new ListNode(0); ListNode tail = dummy; while (a != null && b != null) { if (a.val < b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } if (a != null) { tail.next = a; } else { tail.next = b; } return dummy.next; } }
heap是个最优二叉树, 有以下两个特性
1.结构特性:假设一个二叉树的深度为n。为了满足完全二叉树的要求,该二叉树的前n-1层必须填满,第n层也必须按照从左到右的顺序被填满。即二叉树严格遵循从上到下,再从左到右的方式构造
2.值特性:最大或最小的关系
如果是min heap,则父亲要小于所有的儿子。
max heap, 是父亲要大于所有的儿子。
注意各个儿子之间没有大小关系, 左儿子可能比右儿子大, 也可能小.
以下插入和删除的操作来源于其它博客,侵删。
——————————————————————
插入 add – O(logn)时间:
cs3k.com
在插入操作的时候,会破坏上述堆的性质,所以需要进行名为sift up的操作,以进行恢复。
- 加入一个点放第一个能放的位置, 即最下一层最左的空位。
- 不断和父节点进行比较,直到比父节点小。sift up 操作 :如果new节点比父节点小,那么交换两者。交换之后,继续和新的父节点比较…… 直到new节点不比父节点小,或者new节点成为根节点。
我们插入节点2:
pop() – O(logn)时间:
- 根节点和最下面一层最右的节点换, 然后删了现在的最右下的
- 新的根节点不断和自己的儿子最小的那个比较,然后和儿子中最小的那个换。 直到last节点不大于任一子节点, 或者last节点成为叶节点。
删除操作只能删除根节点。
sift down: 将节点不断的和子节点比较。如果节点比两个子节点中小的那一个大,则和该子节点交换。直到last节点不大于任一子节点,或者last节点成为叶节点。
删除根节点1。如图:
——————————————————————
当我们插入或者删除结点的时候, 就是一路换大或者换小,最多换logN次,所以插入或者删除操作的时间复杂度都是O(logn)
delete() / remove()任意节点 – O(logn)时间:
priority queue由于是阉割版的原因,删除任意节点的时间是O(n), 它只能for一遍, 然后删
heap的任意节点的删除操作是O(logn)时间:
- 用hash map找到这个点, 并把这个点和最下一层的最右互换
- 删了右下的点,然后换过去的和父节点进行比较 >= 父节点, sift up ; < 父节点, sift down
此外:
- hasp map查找一个节点的位置, 构造hash map <key节点的值, value节点在堆里的位置. 所以可以要求堆必须不能有重复数字
- 堆得节点数固定,则堆得形状固定, 用数组就可以储存这个堆:数组第0位存堆的大小,比如一个大小为10的存5个节点的堆:
Array 0 1 2 3 4 5 6 7 8 9 5 1 2 3 4 5
对于一个下标位k的节点:
- 父节点下标 k/2
- 左儿子下标 2k
- 右儿子下标 2k+1
Tree Map
又叫red black tree / balanced binary tree, 所有操作logn
最小一路往左, logn
最大一路往右, logn
priority queque
适合用来解决data stream median 的问题:
Data Stream Median
Numbers keep coming, return the median of numbers at every time a new number added.
Clarification
What’s the definition of Median?
- Median is the number that in the middle of a sorted array. If there are n numbers in a sorted array A, the median is A[(n – 1) / 2]. For example, if A=[1,2,3], median is 2. If A=[1,19], median is 1.
Example
For numbers coming list: [1, 2, 3, 4, 5], return [1, 1, 2, 2, 3].
For numbers coming list: [4, 5, 1, 3, 2, 6, 0], return [4, 4, 4, 3, 3, 3, 3].
For numbers coming list: [2, 20, 100], return [2, 2, 20].
用两个堆, max heap 和 min heap. 维持两个堆的大小相等(max堆可以比min堆多一个). 则max堆的顶即为median值.
Min Stack
cs3k.com
Implement a stack with min() function, which will return the smallest number in the stack.
It should support push, pop and min operation all in O(1) cost.
Notice
min operation will never be called if there is no number in the stack.
Example
Tags
Related Problems
push(1)
pop() // return 1
push(2)
push(3)
min() // return 2
push(1)
min() // return 1
最暴力的解法:
min()的时候for一遍剩下的元素,找到最小的
别担心,勇敢的说,谁的第一反应都是这个。
稍微高级点的解法:
push的时候记录最小值,push+min
-要是加上pop呢?
两个stack,加一个最小值的stack,pop原来的数的时候,也从最小值stack里pop出来一个相应的
Implement Queue by Two Stacks
As the title described, you should only use two stacks to implement a queue’s actions.
The queue should support push(element), pop() and top() where pop is pop the first(a.k.a front) element in the queue.
Both pop and top methods should return the value of first element.
Example
push(1)
pop() // return 1
push(2)
push(3)
top() // return 2
pop() // return 2
准备两个stack,stack1和stack2
放stack1里放正了,再倒到stack2里 就倒过来了,push就push倒stack1里,pop要从stack2里pop
Largest Rectangle in Histogram
cs3k.com
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.
Example
Tags
Related Problems
Given height = [2,1,5,6,2,3],
return 10.
直接的是O(n^{2}) 二重循环(枚举,搜索)
如果想提速的话,可以有两种O(nlogn)和O(n)两个可能
nlogn:
- 二分 for(1….到n, 每次logn)
- 排序 nlogn
- heap logn
二分要排序,但是这道题一排序就乱了
核心点:最矮的那根木头
其实就是for每根木头i,向左摆找第一个比他矮的x,再向右找第一个比他矮的y,
算出height[i]*(y-x-1)
此时,引出了一个坑爹的数据形式:
单调栈
单调栈是个找左边第一个的比它小,右边第一个比它小的。
给出一组数据 2 1 5 6 2 3
流程如下:
先把2塞进去,ok, 比1大
把1塞进去,1比2小,把2踢出来,左边没有,右边第一个比它小的是1
把5塞进去,ok, 比1大
把6塞进去,ok,比1和5 都大
把2塞进去,不行了,比5和6小,从和2接近的踢
即把6踢出去,右边第一个比它小的是2,左边第一个比它小的是5
再把5踢出去,右边第一个比它小的是2,左边第一个比它小的是1
把3塞进去再,ok比1,2大
3是最后一个数,之后再塞一个非正整数的-1
即3被踢出来,右边第一个比它小的是-1,左边是2
接着2被踢出来,右边第一个比它小的是-1,左边是1
接着1被踢出来左边无,右边-1
为了更好的理解单调栈,再跑一组数据,这组我们记录左边和右边第一个比它小的数的index:
4,3,2,1,5,6,2
代码如下:
遇到for()里面又套了一个while的这种要小心,乍一看容易觉得时间复杂度是O(n^{2}), 但是平均复杂度很有可能只是O(n).
因为要分析最坏的情况是不是能每次都发生,是不是为了攒一次最坏的情况,要耗费之前很多次之前的。
这种要蓄一波才能发射的,要算平均。
单调栈适合把O(n^{2})变为O(n)。
Maximal Rectangle
cs3k.com
Given a 2D boolean matrix filled with False and True, find the largest rectangle containing all True and return its area.
Given a matrix:
[
[1, 1, 0, 0, 1],
[0, 1, 0, 0, 1],
[0, 0, 1, 1, 1],
[0, 0, 1, 1, 1],
[0, 0, 0, 0, 1]
]
return 6.
对每一行做底,解前面的矩形问题。
hash table 要懂原理,会实现
cs3k.com
string的hash table 时间复杂度是O(L),其中L是string的长度
O(key的长度)是hash function的时间复杂度
BFS的实现原理是hash map+ queque, 用到了两个最常用的数据结构,所以非常常考。