Repository: nonstriater/Learn-Algorithms Branch: master Commit: 7de8604aa17b Files: 184 Total size: 234.8 KB Directory structure: gitextract_02ozy4cy/ ├── 0 Numeral/ │ ├── base58.md │ └── 数值.md ├── 1 String/ │ ├── KMP.md │ ├── README.md │ └── java String.md ├── 2 List/ │ ├── QPS Counter.md │ ├── README.md │ ├── Redis slowlog.md │ ├── 数组.md │ └── 条件表达式.md ├── 2 Queue/ │ ├── README.md │ ├── ZipList.md │ └── skip-list.md ├── 3 Hash Table/ │ ├── HashMap in Golang.md │ ├── HashMap in Java.md │ ├── HashTable.c │ ├── LinkedHashMap.md │ ├── README.md │ ├── TreeMap in Java.md │ └── hash_ref.c ├── 4 Tree/ │ ├── 1-二叉树 / │ │ ├── btree/ │ │ │ ├── bintree.c │ │ │ ├── btree │ │ │ ├── public.h │ │ │ └── 队列.h │ │ ├── btree.c │ │ ├── rbtree.c │ │ └── suffix_tree.c │ ├── 2-二叉查找树/ │ │ ├── BiSearchTree/ │ │ │ ├── README.md │ │ │ ├── bisearch │ │ │ ├── bisearchtree.c │ │ │ ├── bisearchtree.h │ │ │ └── main.c │ │ └── 二叉查找树.md │ ├── 3-平衡树AVL/ │ │ ├── AVLTree.c │ │ └── README.md │ ├── 4-字典树Trie/ │ │ ├── README.md │ │ └── trie.c │ ├── 5-伸展树/ │ │ └── 伸展树.md │ ├── 6-后缀树/ │ │ └── 后缀树.md │ ├── 7-B树/ │ │ ├── B+树.md │ │ └── B树.md │ ├── 8-堆/ │ │ ├── Top-K 问题.md │ │ ├── heap.c │ │ └── 堆.md │ ├── 9-红黑树 R-B tree/ │ │ └── 红黑树.md │ ├── 92-并查集/ │ │ └── 并查集.md │ ├── README.md │ └── huffman tree/ │ └── 赫夫曼编码.md ├── 5 Graph/ │ ├── DFS 和 BFS.md │ ├── README.md │ ├── 二分图/ │ │ └── 二部图.md │ ├── 拓扑排序.md │ ├── 最小生成树.md │ └── 最短路径.md ├── 6 Sort/ │ ├── 8.c │ ├── README.md │ ├── insert_sort.c │ └── 外排序.md ├── 7 Search/ │ └── README.md ├── 8 Algorithms Analysis/ │ ├── README.md │ ├── 分治算法.md │ ├── 动态规划.md │ ├── 回溯法.md │ ├── 穷举搜索法.md │ ├── 贪心算法.md │ ├── 迭代法.md │ └── 递归.md ├── 9 Algorithms Job Interview/ │ ├── 1 字符串.md │ ├── 1.1 字符串-查找.md │ ├── 1.2 字符串-删除.md │ ├── 1.3 字符串-修改.md │ ├── 1.4 字符串-排序.md │ ├── 2 链表.md │ ├── 2.1 链表-排序.md │ ├── 2.2 链表-删除.md │ ├── 2.3 链表-2条.md │ ├── 3 堆和栈.md │ ├── 4 数值问题.md │ ├── 4.1 数值-加减乘除.md │ ├── 4.2 数值-指数.md │ ├── 4.3 数值-随机数.md │ ├── 4.4 数值-最小公倍数.md │ ├── 4.5 数值-素树.md │ ├── 5 数组数列问题.md │ ├── 5.1 数列-排序.md │ ├── 5.2 数列-nsum问题.md │ ├── 5.3 数列-交并集.md │ ├── 5.4 数列-查找.md │ ├── 6 矩阵.md │ ├── 7 二叉树.md │ ├── 7.1 二叉树-遍历.md │ ├── 7.2 二叉树-建树.md │ ├── 7.5 二叉搜索树.md │ ├── 7.9 树.md │ ├── 8 图.md │ ├── 9 智力思维训练.md │ ├── 91 系统设计.md │ ├── 97 其他.md │ ├── README.md │ ├── codes/ │ │ ├── 1 string/ │ │ │ ├── char_first_appear_once.c │ │ │ ├── proc.c │ │ │ ├── replce_blank.c │ │ │ ├── revert_by_word.c │ │ │ └── string.c │ │ ├── 10.c │ │ ├── 12.c │ │ ├── 20.c │ │ ├── 21.c │ │ ├── 22.c │ │ ├── 23.c │ │ ├── 24.c │ │ ├── 25.c │ │ ├── 26.c │ │ ├── 27.c │ │ ├── 28.c │ │ ├── 4 numer/ │ │ │ ├── Power.c │ │ │ ├── integer_to_bin.c │ │ │ ├── isSquare.c │ │ │ ├── one_appear_count_by_binary.c │ │ │ └── string_to_integer.c │ │ ├── 4-1.c │ │ ├── 5 array/ │ │ │ ├── delete_occurence_character.c │ │ │ ├── factorial.c │ │ │ ├── fibonacci.c │ │ │ ├── longest_continuious_number.c │ │ │ └── print_continuous_sequence_sum.c │ │ ├── 5.c │ │ ├── 6 matrix/ │ │ │ └── print_matrix.c │ │ ├── 7 bianrytree/ │ │ │ ├── binary_search.c │ │ │ └── bt1.c │ │ ├── 7.c │ │ ├── 8.c │ │ ├── c1.c │ │ ├── c10.c │ │ ├── c11-2.c │ │ ├── c11.c │ │ ├── c12.c │ │ ├── c13-1.c │ │ ├── c13-2.c │ │ ├── c14.c │ │ ├── c15.c │ │ ├── c16.c │ │ ├── c17.c │ │ ├── c18.c │ │ ├── c19.c │ │ ├── c2.c │ │ ├── c20.c │ │ ├── c3.c │ │ ├── c4.c │ │ ├── c5.c │ │ ├── c6.c │ │ ├── c7.c │ │ ├── c8.c │ │ ├── c9.c │ │ └── most_visit_ip.c │ ├── 剑指offer/ │ │ └── README.md │ └── 编程之美/ │ └── README.md ├── 91 Algorithms In Big Data/ │ ├── Bitmap.md │ ├── Bloomfilter.md │ ├── Hash映射,分而治之.md │ ├── Inverted Index/ │ │ ├── 倒排索引(Inverted Index).md │ │ └── 数据库索引.md │ ├── README.md │ ├── mapreduce/ │ │ ├── Hash映射,分而治之.md │ │ └── 分布处理之Mapreduce.md │ ├── simhash算法.md │ ├── 双层桶划分.md │ ├── 外排序.md │ └── 海量数据处理.md ├── 92 Algorithms In DB/ │ ├── README.md │ ├── mysql/ │ │ └── README.md │ └── redis/ │ └── README.md ├── 93 Algorithms In Open Source/ │ ├── Bitcoin/ │ │ └── Merkle Tree.md │ ├── GeoHash/ │ │ └── 多维空间点索引算法.md │ ├── README.md │ ├── Timer/ │ │ └── timer.md │ ├── YYKit/ │ │ └── YYCache.md │ ├── kafka/ │ │ └── README.md │ ├── nginx/ │ │ └── README.md │ └── zoomkeeper/ │ └── README.md ├── 94 15-Classic-Algorithms/ │ └── README.md ├── C Language Code Specification.md ├── LICENSE └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: 0 Numeral/base58.md ================================================ # base58 btc中 base58编码表是 ``` 123456789 abcdefg hijk mn opqrst uvwxyz ABCDEFG HJ KLMN PQRST UVWXYZ ``` 相比 base64 去掉了 6个字符, 数字0, 大写字母 O, 大写字母 I (小写i) , 小写字母 l (大写L), 以及 + 和 / ### 编码实现 ``` ``` ================================================ FILE: 0 Numeral/数值.md ================================================ # 数值 `n&(n-1)` 作用是消除数字 n 的二进制表示中的最后一个 1 一个数和它本身做异或运算(^)结果为 0,即 `a ^ a = 0` 不用临时变量交换两个数 ``` int a = 1, b = 2; a ^= b; b ^= a; a ^= b; // 现在 a = 2, b = 1 ``` ================================================ FILE: 1 String/KMP.md ================================================ # 字符串匹配算法 KMP KMP于1977年被提出,全称 Knuth–Morris–Pratt 算法; 名字分别是:Donald Knuth(K), James H. Morris(M), Vaughan Pratt(P). KMP算法是一种字符串匹配算法,可以在 O(n+m) 的时间复杂度内实现两个字符串的匹配。 ================================================ FILE: 1 String/README.md ================================================ # 字符串 字符串在计算机中的应用非常广泛,这里讨论有关字符串的最重要的算法: * 排序 * 查找 * 单词查找树 * 子串查找 * 正则表达式:正则表达式是模式匹配的基础,是一个一般化了的子字符串的查找问题,也是搜索工具grep的核心。 * 模式匹配 * grep * 数据压缩 * 赫夫曼树 * 游程编码 [Java中 String实现 参考这里](java%20String.md) ## 查找 * KMP * BM ## 滑动窗口 使用滑动窗口解决字符串子串问题,代码框架 ``` int left = 0, right = 0; while (right < s.size()) { // 增大窗口 window.add(s[right]); right++; while (window needs shrink) { // 缩小窗口 window.remove(s[left]); left++; } } ``` ## 参考 《Algorithms》 ================================================ FILE: 1 String/java String.md ================================================ # java String Java 中 String 实现。 ================================================ FILE: 2 List/QPS Counter.md ================================================ # QPS Counter 统计各接口QPS计数 使用一个双向循环链表结构 ```Java static AtomicInteger qpsCount = 100; //线程安全 static volatile long lastSenconds = System.currentTimeMillis()/1000; //1 计数器 public static boolean tryAcquire() { long current = System.currentTimeMillis()/1000; if(current == lastSenconds){ if (qpsCount-- > 0) {//CAS api return true; } else { //限流 return false; } } else{//下一个时间窗口 lastSenconds = current; qpsCount = 100; return true; } } ``` ================================================ FILE: 2 List/README.md ================================================ # 链表 * 链表 * 双向链表 * 双向循环链表 ## 链表 ### 应用场景 * [redis slowlog](Redis%20slowlog.md) ### 链表 VS 数组 特点 * 数组 : 内存连续, 更好利用局部性原理;内存空间必须一次性分配够,所以说数组如果要扩容,需要重新分配一块更大的空间,再把数据全部复制过去,有界 * 链表 : 不存在数组的扩容问题, 空间不连续,你无法根据一个索引算出对应元素的地址,所以不能随机访问; 需要前后元素位置的指针,会消耗相对更多的储存空间 优缺点: * 查询: 数组 O(1), 有序时可以用二分查找; * 删除: 链表只需要移动指针 O(1) ,数组的话删除元素需要移动后续的元素 O(N) ### 扩缩容 简单说下编程语言 java, golang中 LinkList的扩缩容的策略。 java 中扩容,每次扩容新增原先容量的 1/2 ``` int newCapacity = oldCapacity + (oldCapacity >> 1); ``` 这个就不介绍了。重点说下双向链表。 ## 双向链表 双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。 双向链表克服了单链表中访问某个节点前驱节点(插入,删除操作时),只能从头遍历的问题。 缺点是: 多了1倍的额外的指针空间大小。 ``` typedef int Value typedef struct Entry{ struct Entry *next,*prev; Value value; }DoubleLink; ``` 应用场景 * mysql B+树 叶子节点就使用 双向链表,方便 `age<10` 类似条件查询,或者 倒序查询 如 `order by desc` ,从后向前遍历数据 * Java AQS 中的等待队列, 是一个双端 双向链表 的结构 (FIFO 结构) ## 循环链表 最后一个节点指针指向头节点的链表 [QPS 计数器实现](QPS%20Counter.md) ## 双向循环链表 ================================================ FILE: 2 List/Redis slowlog.md ================================================ # Redis slowlog redis中的slowlog使用链表来保存 ``` struct redisServer { // ... // 下一条慢查询日志的 ID long long slowlog_entry_id; // 保存了所有慢查询日志的链表 list *slowlog; // 服务器配置 slowlog-log-slower-than 选项的值 long long slowlog_log_slower_than; // 服务器配置 slowlog-max-len 选项的值 unsigned long slowlog_max_len; // ... }; ``` 一条slowlog entry标识 ``` typedef struct slowlogEntry { // 唯一标识符 long long id; // 命令执行时的时间,格式为 UNIX 时间戳 time_t time; // 执行命令消耗的时间,以微秒为单位 long long duration; // 命令与命令参数 robj **argv; // 命令与命令参数的数量 int argc; } slowlogEntry; ``` ================================================ FILE: 2 List/数组.md ================================================ # 数组 数据与链表差异 [参考这里](README.md) ## 循环数组 特点 * 长度固定,下表不会越界,使用2个指针标识 头尾 下标; 默认都是 0 * 方便用来实现 栈,队列;比如 队列实现时,新增元素,头下表+1; 删除元素,尾下标+1 比如Java的 ArrayBlockingQueue 就是一个 带有 takeIndex 和 putIndex 的环形数组。 缺点:头尾之间的元素不好维护,从中间删除了某个元素,会出现数组空隙。 数据结构 ``` ``` ================================================ FILE: 2 List/条件表达式.md ================================================ # 条件表达式 ### 应用场景 * SQL 语句中的条件 * 广告系统中配置定向的过滤条件 * 编译器语法解析 DNF 析取范式 ================================================ FILE: 2 Queue/README.md ================================================ # 队列 Queue ================================================ FILE: 2 Queue/ZipList.md ================================================ # ZipList ZipList 压缩列表 redis 中的 sset(有序集合) 使用跳表来实现,为什么不是用红黑树,而是跳表实现sset,带着这样的疑问,有了本文。 ## 理解 有哪些应用场景? #### 为什么使用 ## 结构 ## 插入,删除,查找 实现 ================================================ FILE: 2 Queue/skip-list.md ================================================ # skip-list skip-list 跳表。 redis 中的 sset(有序集合) 使用跳表来实现,为什么不是用红黑树,而是跳表实现sset,带着这样的疑问,有了本文。 ## 跳表理解 有哪些应用场景? ## 跳表的结构 ## 插入,删除,查找 实现 ## sset 为什么使用 跳表 ================================================ FILE: 3 Hash Table/HashMap in Golang.md ================================================ # HashMap in Golang java 中 hashmap的实现原理 ================================================ FILE: 3 Hash Table/HashMap in Java.md ================================================ # HashMap in Java java 中 hashmap的实现原理。 [红黑树参考这里](../4%20Tree/9-红黑树%20R-B%20树) ## 数据结构 * HashMap底层实现, hashmap的存储结构和操作? * hash冲突如何解决(链表和红黑树)? 为什么hashmap中的链表需要转成红黑树? 好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。 ``` public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { transient int size; //当前元素个数 int threshold; //扩容时机是: 当前容量大于等于 capacity * load factor final float loadFactor; //默认 0.75 , 空间使用 75% 时开始扩容 static final int TREEIFY_THRESHOLD = 8; //将链表转换为红黑树的阈值 static final int UNTREEIFY_THRESHOLD = 6; //将红黑树转换为链表的阈值 //1.7结构 static class Node implements Map.Entry { final int hash; final K key; V value; Node next;//链表结构 ... } //1.8结构 static final class TreeNode extends LinkedHashMap.Entry { TreeNode parent; // red-black tree links TreeNode left; TreeNode right; TreeNode prev; // needed to unlink next upon deletion boolean red; } public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value; } //hash 算法, 根据 key 的 hashCode() 计算而来 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } } ``` table 在第一次put的时候初始化,length默认16, threshold 12 (0.75x16) , 也就是第 13个 元素 put 的时候开始扩容 1.7 的实现是 数组+链表; 1.8 新增了红黑树,提高查询效率 ### get(key) 方法 ``` public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode)//红黑树 查找 return ((TreeNode)first).getTreeNode(hash, key); do {//按照链表结构遍历查找 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } ``` ``` //计算hash值 //这是一个神奇的函数,用了很多的异或,移位等运算,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } ``` ### put(key, value) 方法 hash存储的过程是: key -> hashcode -> hash -> indexFor() ``` public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null)//当前桶为空,没有hash冲突 tab[i] = newNode(hash, key, value, null); else { Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//当前桶中的 key、key 的 hashcode 与写入的 key 相等 e = p; else if (p instanceof TreeNode)//当前桶为红黑树,那按照红黑树的方式写入数据 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else {//链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);//判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))//在遍历过程中找到 key 相同时直接退出遍历 break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold)//最后判断是否需要进行扩容 resize(); afterNodeInsertion(evict); return null; } ``` ``` //返回数组下标 static int indexFor(int h, int length) { return h & (length-1); } ``` 这里用的位运行,而不是取模操作; 位运算性能更高。 ## Hash冲突 HashMap是怎么处理hash碰撞的? 使用拉链法,为了提高链表查询效率,当桶对应的链表长度大于8的时候,转为红黑树。 ## 扩容 初始化容量, 默认结果是 16 ``` static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16, 为啥用位运算呢?直接写16不好么? ``` ### 为什么需要扩容? 主要为缓解哈希冲突造成的外挂链表太长,造成查询性能低下。 ### HashMap的扩容方式? 负载因子是多少? 扩容时机?什么时候会触发扩容? HashMap 中 `final float loadFactor` , loadFactor 默认 0.75 , 也就是达到容量的 75%时就会开始扩容。 那么问题来了,扩容到多大? 看 resize() 方法 ``` final Node[] resize() { Node[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; //创建一个新的 newCap 大小的数组容器,调整table @SuppressWarnings({"rawtypes","unchecked"}) Node[] newTab = (Node[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) {//有数据的桶才需要调整 oldTab[j] = null; if (e.next == null)//没有链表结构 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode)e).split(this, newTab, j, oldCap); else { // preserve order Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; if ((e.hash & oldCap) == 0) {//hash值第 N+1 位是否为0 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } ``` 扩容到了2倍 `newThr = oldThr << 1; ` ### 扩容后元素怎么重排到新的容器中,直接复制拷贝可以吗? 扩容会 rehash,复制数据等耗时操作。 * hashmap扩容时每个entry需要再计算一次hash吗? 不需要,但是需要重新调整桶的位置 `newTab[e.hash & (newCap - 1)] = e;` * 扩容时避免rehash的优化 : cap 为 2 的次幂,rehash也是 cap * 2 , 这样可以 `e.hash & (newCap - 1)` 计算时,有较少的 key 产生移动,hash值 高位 1 的 才需要移动 ## 问题 ### JDK7 和 8 HashMap 有什么区别? * JDK8 实现引入红黑树,优化链表过长的查询效率;当链表长度大于8是链表的存储结构会被修改成红黑树的形式, 查询效率从O(N)提升到O(logN)。链表长度小于6时,红黑树的方式退化成链表。 * 1.7 采用头插法, 链表插入是从链表头部插入 ; 1.8采用尾插法, 因此在resize的时候仍然保持原来的顺序; 解决并发多线程中出出现链表成环的问题 ; (hashmap本来就是非线程安全的,为啥要在多线程中使用hashmap) ### 链表上使用的头插还是尾插方式? 1.7 采用头插法,1.8采用尾插法; 解决多线程中出出现链表成环的问题, 因此在resize的时候仍然保持原来的顺序 ### 多线程下死循环问题 * hashmap扩容会引发什么问题,线上是否出现过类似的问题?如何避免扩容引发的问题? * jdk1.8之前并发操作hashmap时为什么会有死循环的问题? HashMap 在并发场景下,容易出现死循环 ``` final HashMap map = new HashMap(); for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { map.put(UUID.randomUUID().toString(), ""); } }).start(); } ``` 在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。程序临床反应就是 CPU 飙高, 这时候应该使用线程安全的HashMap,也就是 ConcurrentHashMap。 ### hashmap的数组长度为什么要保证是2的幂? https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4631373 提出, 整数的除法(/)和 取模(%) 运算 性能比 位与操作(&) 慢10倍。 `h & (length-1)` 和 `h % length` 结果一样 举个例子 ``` hashcode 311 对应的二进制是(1 0011 0111) length 16 对应的二进制是(1 0000) , length-1 就是 (0 1111) h & (length-1)` 就是取 hashcode 的低 4位 ``` length 保持为 2 的幂, 那么length-1就会变成一个mask, 它会将hashcode低位取出来,hashcode的低位实际就是余数,和取余操作相比,与操作会将性能提升很多。 另外,hash扩容时 rehash 操作,只有 hash二进制 高位是 1 的hash key 需要 移动到新的 slot (pos + oldCap), 高位是 0 的 key 不需要移动 ![hashmap rehash](https://pic3.zhimg.com/80/v2-ed0ca17db342562dfc18434d12227be2_720w.jpg) ### 重写equals方法需同时重写hashCode方法 ``` public static void main(String []args){ HashMap map = new HashMap(); Person person = new Person(1234,"乔峰"); //put到hashmap中去 map.put(person,"天龙八部"); //get取出,从逻辑上讲应该能输出“天龙八部” System.out.println("结果:"+map.get(new Person(1234,"萧峰"))); } ``` hashCode()的默认行为是对堆上的对象产生独特值。因此 map.get(object) 的时候,不同的 object 的hashcode 不一样,自然结果就为null ================================================ FILE: 3 Hash Table/HashTable.c ================================================ #include "stdio.h" typedef char * Key; typedef int Value; typedef unsigned int Hash; typedef struct Entry { struct Entry *next; Hash hash; // key 对应的hash值 Key key; Value value; }Entry; // 拉链法实现,也就是链表的数组.也是 数组和链表优势的结合 typedef struct HashTable{ Entry **head; unsigned int size; unsigned int usage; }HashTable; HashTable *create(unsigned int); HashTable *put(HashTable *,Key,Value); HashTable *putInHeads(HashTable *,Key,Value); // 数组 HashTable *putInLists(HashTable *,Key,Value); // 链表 Value find(HashTable *,Key ); HashTable*delete(HashTable *,Key ); void print(HashTable *); Hash hashCode(Key); // 哈希函数 HashTable *create(unsigned int size){ HashTable *hashTable = malloc(sizeof(HashTable)); if(hashTable==NULL){ printf("malloc error\n"); return NULL; } hashTable->size=size; hashTable->usage = 0; hashTable->head = calloc(size,sizeof(Entry *)); return hashTable; } HashTable *put(HashTable *hashTable,Key key,Value value){ if (key==NULL) { return hashTable; } } Value find(HashTable *hashTable,Key key){ } void print(HashTable *hashTable){ Entry *entry; for (int i = 0; i < hashTable->size; ++i) { entry = hashTable->head[i]; while(entry!=NULL){ printf("%s=%d\n",entry->key,entry->value); entry=entry->next; } } } // ????? Hash hashCode(char *key){ int offset = 5; Hash hashCode = 0; while(*key){ hashCode = (hashCode << offset) + (*key++); } return hashCode; } int main(){ return 0; } ================================================ FILE: 3 Hash Table/LinkedHashMap.md ================================================ # LinkedHashMap java 中 LinkedHashmap的实现原理。LinkedHashmap继承自 HashMap。 HashMap是无序的,迭代访问顺序并不一定与插入(put)顺序一致。LinkedHashMap 是有序的, 迭代顺序与插入顺序一致,这种叫做 插入有序。 ``` //插入有序 Map linkedHashMap = new LinkedHashMap<>(); linkedHashMap.put("name1", "josan1"); linkedHashMap.put("name2", "josan2"); linkedHashMap.put("name3", "josan3"); Set> set = linkedHashMap.entrySet(); Iterator> iterator = set.iterator(); while(iterator.hasNext()) { Entry entry = iterator.next(); String key = (String) entry.getKey(); String value = (String) entry.getValue(); System.out.println("key:" + key + ",value:" + value); } output: key:name1, value:josan1 key:name2, value:josan2 key:name3, value:josan3 ``` #### 特点 * 维护一个所有entry的双向链表 * 构造函数 有一个 accessOrder 参数,控制访问顺序 (插入顺序 和 访问顺序); * 访问顺序的意思是,当有一个entry被访问以后,这个entry就被移动到链表的表尾。这个特性非常适合 LRU 缓存 (最近最少使用); #### 引用场景 * LUR 缓存 (最近最少使用) ## 原理 ``` public class LinkedHashMap extends HashMap implements Map { static class Entry extends HashMap.Node { Entry before, after; Entry(int hash, K key, V value, Node next) { super(hash, key, value, next); } } transient LinkedHashMap.Entry head; transient LinkedHashMap.Entry tail; //构造函数如下, public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; } } ``` ## 用LinkedHashMap实现LRU 构造函数中 accessOrder 参数是控制LinkedHashMap 访问顺序,默认为插入顺序(false), true 代表访问顺序。 访问顺序的意思是,当有一个entry被访问以后,这个entry就被移动到链表的表尾。 这个特性非常适合 LRU 缓存 (最近最少使用) ; 插入逻辑,运行自定义删除最老entry的逻辑 ``` void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); } } ``` 重写此方法,维持此映射只保存100个条目的稳定状态,在每次添加新条目时删除最旧的条目。 ``` private static final int MAX_ENTRIES = 100; protected boolean removeEldestEntry(Map.Entry eldest) { return size() > MAX_ENTRIES; } ``` 基于 LinkedHashMap实现的 LRU Cache ```Java class LRUCache { int cap; LinkedHashMap cache = new LinkedHashMap<>(); public LRUCache(int capacity) { this.cap = capacity; } //访问元素 public int get(int key) { if (!cache.containsKey(key)) { return -1; } // 将 key 变为最近使用 makeRecently(key); return cache.get(key); } //添加元素到cache public void put(int key, int val) { if (cache.containsKey(key)) { // 修改 key 的值 cache.put(key, val); // 将 key 变为最近使用 makeRecently(key); return; } if (cache.size() >= this.cap) { // 链表头部就是最久未使用的 key int oldestKey = cache.keySet().iterator().next(); cache.remove(oldestKey); } // 将新的 key 添加链表尾部 cache.put(key, val); } private void makeRecently(int key) { int val = cache.get(key); // 删除 key,重新插入到队尾 cache.remove(key); cache.put(key, val); } } ``` ================================================ FILE: 3 Hash Table/README.md ================================================ # 散列表 本节围绕以下内容展开: * 散列表 * 散列函数设计 * 冲突处理 * hashmap数据结构 [Golang 中HashMap实现](HashMap%20in%20Golang.md) [Java 中HashMap实现](HashMap%20in%20Java.md) [Java 中LinkedHashMap实现](LinkedHashMap.md) [Java 中TreeMap实现](TreeMap%20in%20Java.md) 散列表使用某种算法操作(散列函数)将键转化为数组的索引来访问数组中的数据,这样可以通过Key-value的方式来访问数据,达到常数级别的存取效率。现在的nosql数据库都是采用key-value的方式来访问存储数据。 散列表是算法在时间和空间上做出权衡的经典例子。通过一个散列函数,将键值key映射到记录的访问地址,达到快速查找的目的。如果没有内存限制,我们可以直接将键作为数组的索引,所有的操作操作只需要一次访问内存就可以完成。但这种情况不太现实。 ## Hashmap应用 1. cocos2d 游戏引擎 CCScheduler 2. linux 内核bcache。 缓存加速技术,使用SSD固态硬盘作为高速缓存,提高慢速存储设备HDD机械硬盘的性能 3. hash表在海量数据处理中有广泛应用。如海量日志中,提取出某日访问百度次数最多的IP 4. Java 中HashMap实现。编程语言中HashMap是如何实现的呢? 说说 Java , Golang 5. redis hash结构, set通常也是基于Hash结构实现 ## 散列函数 散列函数就是将键转化为数组索引的过程。且这个函数应该易于计算且能够均与分布所有的键。 散列函数最常用的方法是`除留余数法`。这时候被除数应该选用`素数`,这样才能保证键值的均匀散布。 散列函数和键的类型有关,每种数据类型都需要相应的散列函数;比如键的类型是整数,那我们可以直接使用`除留余数法`;这里特别说明下,大多数情况下,键的类型都是字符串,这个时候我们任然可以使用`除留余数法`,将字符串当做一个特别大的整数。 ``` int hash = 0; for (int i=0;isize = size; hashMap->usage = 0; hashMap->heads = calloc(size,sizeof(Entry *)); return hashMap; } HashMap *put(HashMap *hashMap,Key key,Value value){ if (key == NULL){ return hashMap; } Hash hash = hashCode(key); int index = hash & (size-1);/* */ if (hashMap->heads[index] == NULL){ _putInHead(hashMap,index,key,value); }else{ _putInList(hashMap,index,key,value); } } Value get(HashMap hashMap*,Key key){ if (key == NULL){ return hashMap; } Hash hash = hashCode(key); int index = hash & (size-1);/* */ Entry *entry = hashMap->heads[index]; while(entry != NULL){ if (entry->hash == hash){ return entry->value; } entry = entry->next; } return NULL; } HashMap *_putInHead(HashMap *hashMap,int index,Key key,Value value){ Entry *newHead = malloc(sizeof(Entry)); newHead->hash = hash; newHead->key = key; newHead->value = value; hashMap->heads[index] = newHead; (hashMap->usage)++; return hashMap; } HashMap *_putInList(HashMap *hashMap,int index,Key key,Value value){ Entry *lastEntry = hashMap->heads[index]; while(lastEntry != NULL){ if (lastEntry->hash == hash){ return hashMap; }else{ lastEntry = lastEntry->next; } } lastEntry = malloc(sizeof(Entry)); lastEntry->hash = hash; lastEntry->key = key; lastEntry->value = value; lastEntry->next = hashMap->heads[index]; hashMap->heads[index] = lastEntry; (hashMap->usage)++; return hashMap; } ``` ### 扩容 当hash表保存的键值对数量太多或太少,对hash 表进行扩容和缩容。合理控制内存的使用。 redis hash中, rehash发生在扩容或缩容阶段,扩容是发生在元素的个数等于哈希表数组的长度时,进行2倍的扩容;缩容发生在当元素个数为数组长度的10%时,进行缩容 ## 参考 《Algorithms》 ================================================ FILE: 3 Hash Table/TreeMap in Java.md ================================================ # TreeMap in Java java 中 TreeMap的实现原理。 #### 特点 * 有序的Key-value 集合,通过红黑树实现; * 遍历时元素按照键key的自然顺序进行排序,也可以创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法 * TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) #### 引用场景 * 一致性Hash算法 ## 使用 ``` //创建TreeMap对象: TreeMap treeMap = new TreeMap(); //新增元素: treeMap.put("hello",1); treeMap.put("world",2); treeMap.put("my",3); treeMap.put("name",4); //遍历元素, 按照key排序 Set> entrySet = treeMap.entrySet(); for(Map.Entry entry : entrySet){ String key = entry.getKey(); Integer value = entry.getValue(); System.out.println("TreeMap元素的key:"+key+",value:"+value); } String firstKey = treeMap.firstKey();//获取集合内第一个元素 String lastKey =treeMap.lastKey();//获取集合内最后一个元素 String lowerKey =treeMap.lowerKey("jiaboyan");//获取集合内的key小于"jiaboyan"的第一个key String ceilingKey =treeMap.ceilingKey("jiaboyan");//获取集合内的key大于等于"jiaboyan"的第一个key ``` ## 原理 参考 [红黑树](../Tree/9-红黑树\ R-B\ tree/红黑树.md) ## 应用 ### 一致性Hash算法 一致性Hash算法解决的问题是: 数据均匀的分片存储在不同的机器节点上,且在机器节点上发送增删时 (扩容或者缩容时) ,最少数据集的映射(rehash) 规则发生改变。 简单说下原理: treeMap 中 key 是 机器节点node 的 hash值, value 是机器节点 IP:port ; 使用TreeMap的 ceilingKey(hash) 这个 API 可以获得 第一个大于 这个 hash值的 节点 ``` public class Demo { private static String[] servers = {“ip1”, “1p2”, “ip3"}; private TreeMap treeMap; // /* 一个数据key,会被分片到哪个机器上 */ public String shardingServer(String key) { int dataHash = hash(key); //怎么找大于 data_hash 值 的第一个节点? 借助 TreeMap 结构 String node = getServer(dataHash) return node; } /* hash 函数*/ public int hash(String key){ } //寻找第一个大于 hash 值的 node private String getServer(String hash) { return treeMap.ceilingKey(hash) } } ``` ================================================ FILE: 3 Hash Table/hash_ref.c ================================================ #include "stdio.h" #include "stdlib.h" #include "string.h" #include "time.h" #define DEFAULT_MAP_SIZE 1<<5 #define MAX_MAP_SIZE 1<<30 #define CAPACITY_FACTOR 0.6f #define MAX_KEY_LENGTH 1<<3 typedef unsigned int Hash; typedef char* Key; typedef int Value; typedef struct Entry{ struct Entry *next; Hash hash; Value value; Key key; }Entry; typedef struct HashMap{ Entry** heads; Value nul; unsigned int size; unsigned int usage; unsigned int realCapacity; //size * CAPACITY_FACTOR unsigned int sizeForIndex; //size - 1 }HashMap; HashMap* create(unsigned int); HashMap* transfer(HashMap*, HashMap*); Hash hash(char[]); HashMap* put(HashMap*, Key, Value); HashMap* putInHeads(HashMap*, int, Hash, Key, Value); HashMap* putInList(HashMap*, int, Hash, Key, Value); HashMap* resize(HashMap*); int find(char[]); void print(HashMap*); HashMap* create(unsigned int size){ HashMap* hashMap = malloc(sizeof(HashMap)); int i; hashMap->size = size; hashMap->sizeForIndex = size - 1; hashMap->realCapacity = (unsigned int)(size * CAPACITY_FACTOR); hashMap->usage = 0; hashMap->heads = calloc(size, sizeof(Entry*)); hashMap->nul = 0; return hashMap; } HashMap* transfer(HashMap* newHashMap, HashMap* oldHashMap){ unsigned int oldIndex, newIndex; Entry *oldEntry, *oldNextEntry; for (oldIndex = 0; oldIndex < oldHashMap->size; oldIndex++){ if (oldHashMap->heads[oldIndex] != NULL){ oldEntry = oldHashMap->heads[oldIndex]; do{ newIndex = (oldEntry->hash) & (newHashMap->sizeForIndex); oldNextEntry = oldEntry->next; oldEntry->next = newHashMap->heads[newIndex]; newHashMap->heads[newIndex] = oldEntry; oldEntry = oldNextEntry; } while (oldEntry != NULL); } } newHashMap->usage = oldHashMap->usage; newHashMap->nul = oldHashMap->nul; free(oldHashMap->heads); oldHashMap->heads = NULL; free(oldHashMap); oldHashMap = NULL; return newHashMap; } Hash hashCode(char* key){ int offset = 5; Hash hashCode = 0; while (*key) hashCode = (hashCode << offset) + (*key++); return hashCode; } HashMap* put(HashMap* hashMap, Key key, Value value){ if (key == NULL){ hashMap->nul = value; return hashMap; } Hash hash = hashCode(key); int index = hash & (hashMap->sizeForIndex); if (hashMap->heads[index] == NULL){ return putInHeads(hashMap, index, hash, key, value); } else { return putInList(hashMap, index, hash, key, value); } } HashMap* putInHeads(HashMap* hashMap, int index, Hash hash, Key key, Value value){ Entry* newHead = malloc(sizeof(Entry)); newHead->hash = hash; newHead->key = _strdup(key); newHead->value = value; newHead->next = NULL; hashMap->heads[index] = newHead; (hashMap->usage)++; if (hashMap->usage > hashMap->realCapacity) return resize(hashMap); return hashMap; } HashMap* putInList(HashMap* hashMap, int index, Hash hash, Key key, Value value){ Entry* lastEntry = hashMap->heads[index]; while (lastEntry != NULL){ if (lastEntry->hash == hash){ lastEntry->value += value; return hashMap; } else lastEntry = lastEntry->next; } lastEntry = malloc(sizeof(Entry)); lastEntry->hash = hash; lastEntry->key = _strdup(key); lastEntry->value = value; lastEntry->next = hashMap->heads[index]; hashMap->heads[index] = lastEntry; (hashMap->usage)++; if (hashMap->usage > hashMap->realCapacity) return resize(hashMap); return hashMap; } HashMap* resize(HashMap* hashMap){ HashMap* newHashMap; if ((hashMap->size << 1) <= MAX_MAP_SIZE) { newHashMap = create((hashMap->size << 1)); newHashMap = transfer(newHashMap, hashMap); return newHashMap; } return hashMap; } Value get(HashMap* hashMap, Key key){ Hash hash = hashCode(key); int index = hash & (hashMap->sizeForIndex); Entry* entry = hashMap->heads[index]; while (entry != NULL){ if (entry->hash == hash) return entry->value; entry = entry->next; } return NULL; } void print(HashMap* hashMap){ unsigned int i; Entry* entry; for (i = 0; i < hashMap->size; i++){ entry = hashMap->heads[i]; while (entry != NULL){ printf("%s: %d\n", entry->key, entry->value); entry = entry->next; } } } int main(){ clock_t start, end; HashMap* hashMap = create(DEFAULT_MAP_SIZE); FILE* fp; char filePath[0xFF]; char line[5]; char key[MAX_KEY_LENGTH]; printf("input file path: "); scanf_s("%s", filePath, 0xFF); if (fopen_s(&fp, filePath, "r") == 0){ start = clock(); while (fgets(line, 5, fp) != NULL){ line[3] = '\0'; hashMap = put(hashMap, line, 1); } end = clock(); printf("\n%fs", (float)(end - start)/1000); fclose(fp); print(hashMap); } else { printf("can't open file"); } return 0; } ================================================ FILE: 4 Tree/1-二叉树 /btree/bintree.c ================================================ /** * 第一次题目 :构建二叉树(链表存储方式),空格表示空树,实现二叉树的基本操作:创建,遍历(先,中,后,按层)二叉树 * 提交邮箱 :510495266@qq.com to learn_algorithm@163.com * 邮件题目 :第一期第一次作业 **/ #include #include typedef struct BiTNode{ char item; struct BiTNode *lChild,*rChild; }BiTNode,*BiTree; // ========================================================================== #define OK 1 #define ERROR 0 typedef int bool; typedef BiTree ElemType ; // 也可能是一个复杂的复合类型 typedef int Status; //队列的顺序存储 typedef struct Node{ ElemType *elem; ElemType *head; ElemType *tail; int length; // 当前队列的长度 int size; // 队列容器的长度,在队列慢得时候可以扩容 }SqQueue; // // 在队列采用顺序存储时,有一个毛病,就是队列操作一段时间后,头指针到了队列容器的尾部,而头指针前面的容器内存不可用了, //造成内存极大的浪费,这个问题可以通过循环队列来解决。但是在 // 链式队列上则不存在这样的问题 // typedef struct QNode{ struct QNode *next; ElemType elem; }LinkQueue; // 队列需要维护两个 指针 (队头指针,队尾指针) typedef struct{ LinkQueue *head; LinkQueue *tail; int length; }Queue; Status initQueue(Queue *queue);// 带头结点,没有引用传值,就用指针的指针吧 bool isEmpty(Queue *q); int length(Queue *q); // 在头部插入,尾部删除 Status getHead(Queue *q,ElemType *e); Status enQueue(Queue *q,ElemType e); Status deQueue(Queue *q,ElemType *e); void traveser(Queue *q); Status initQueue(Queue *queue){// 带头结点,没有引用传值,就用指针的指针吧 LinkQueue *lq = (LinkQueue *)malloc(sizeof(LinkQueue)); if (!lq) { return ERROR; } lq->elem = NULL; lq->next = NULL; (queue)->head = (queue)->tail = lq; // -> 优先级高于 * ,老老实实打上括号 (queue)->length = 0; return OK; } bool isEmpty(Queue *q){ return (q->head == q->tail); } int length(Queue *q){ // int ret; // LinkQueue *p = q->head; // while (p != q->tail) { // ret++; // p = p->next; // // } return q->length; } Status getHead(Queue *q,ElemType *e){ LinkQueue *p = q->head->next; if (!p) { *e=NULL; return ERROR; } *e = p->elem; return OK; } // 入队(加入到队尾) Status enQueue(Queue *q,ElemType e){ LinkQueue *newNode = (LinkQueue *)malloc(sizeof(LinkQueue)); if (!newNode) { return ERROR; } newNode->elem = e; newNode->next = NULL; q->tail->next = newNode; q->tail = newNode; q->length++; return OK; } //出队(队头). 注意,可能只有1个元素,造成队尾指针丢失 Status deQueue(Queue *q,ElemType *e){ LinkQueue *p = q->head->next; if (!p) {// 队列空 return ERROR; } if (e) { *e = p->elem ; } LinkQueue *temp = p->next; q->head->next = temp; if (p == q->tail) { q->tail = q->head; } free(p); q->length--; return OK; } void traveser(Queue *q){ LinkQueue *p = q->head->next; while (NULL != p) { //printf("%d\n",p->elem); p=p->next; } } // ================================================== /* int CreateBiTree(BiTree *T) { *T = (BiTNode *)malloc(sizeof(BiTNode)); if (*T==NULL) { printf("memery malloc failure !\n"); return -1; } printf("enter data to create node:\n"); scanf("%c",&((*T)->item)); if((*T)->item=='#'){ *T=NULL; } if(*T){ printf("创建左子树:\n"); CreateBiTree( &((*T)->lChild) ); printf("创建右子树:\n"); CreateBiTree( &((*T)->rChild) ); } return 0; } */ // BiTree CreateBiTree() // 先序遍历方式创建二叉树 BiTree CreateBiTree(){ char c; BiTNode *tree; // 过滤回车键 scanf("%c",&c); if (c==' ') { printf("创建空节点\n"); tree = NULL; }else{ printf("创建节点 %c\n",c); tree = (BiTNode *)malloc(sizeof(BiTNode)); tree->item = c; tree->lChild = CreateBiTree(); tree->rChild = CreateBiTree(); } return tree; } int PreOrderTraverse(BiTree T){ if(T){ printf("%c\n",T->item ); PreOrderTraverse(T->lChild); PreOrderTraverse(T->rChild); } return 0; } int InOrderTraverse(BiTree T){ if (T) { InOrderTraverse(T->lChild); printf("%c\n", T->item); InOrderTraverse(T->rChild); } return 0; } int PostOrderTraverse(BiTree T){ if (T) { PostOrderTraverse(T->lChild); PostOrderTraverse(T->rChild); printf("%c\n",T->item ); } return 0; } // 广度优先遍历 (队列实现) int LevelOrderTraverse(BiTree T){ if (T) { Queue queue; initQueue(&queue); BiTree u; u=(BiTree)malloc(sizeof(BiTNode)); enQueue(&queue, T); while(!isEmpty(&queue)) { deQueue(&queue, &u); printf("%c",u->item); if(u->lChild) enQueue(&queue, u->lChild); if(u->rChild) enQueue(&queue, u->rChild); } } return 0; } int main(int argc, char const *argv[]) { /* code */ BiTree binaryTree; printf("创建二叉树,输入\"空格\"创建空节点(先序方式建立二叉树):\n"); binaryTree = CreateBiTree(); if(binaryTree==NULL){ printf("创建空的二叉树\n"); } printf("前序遍历:\n"); PreOrderTraverse(binaryTree); printf("\n\n"); printf("中序遍历:\n"); InOrderTraverse(binaryTree); printf("\n\n"); printf("后序遍历:\n"); PostOrderTraverse(binaryTree); printf("\n\n"); printf("层序遍历:\n"); LevelOrderTraverse(binaryTree); printf("\n\n"); return 0; } ================================================ FILE: 4 Tree/1-二叉树 /btree/public.h ================================================ // // public.h // DS // // Created by mac on 13-9-8. // Copyright (c) 2013年 xiaoran. All rights reserved. // #ifndef DS_public_h #define DS_public_h //#include #include "stdlib.h" // 后来,换到mac环境下,#include 应该为 或 #include "stdlib.h" #include #include // for memset typedef char ElemType ; // 也可能是一个复杂的复合类型 typedef int Status; #define OK 1 #define ERROR 0 typedef int bool; #define YES 1 #define NO 0 #define DEFAULT_SIZE 10 #define OUT_OF_BOUND -1 #endif ================================================ FILE: 4 Tree/1-二叉树 /btree/队列.h ================================================ // // 队列.h // 只在一段进行插入,另一端删除元素 // // Created by mac on 13-9-8. // Copyright (c) 2013年 xiaoran. All rights reserved. // #ifndef DS____h #define DS____h #include "public.h" #include "bintree.c" typedef BiTNode ElemType; //队列的顺序存储 typedef struct Node{ ElemType *elem; ElemType *head; ElemType *tail; int length; // 当前队列的长度 int size; // 队列容器的长度,在队列慢得时候可以扩容 }SqQueue; // // 在队列采用顺序存储时,有一个毛病,就是队列操作一段时间后,头指针到了队列容器的尾部,而头指针前面的容器内存不可用了, //造成内存极大的浪费,这个问题可以通过循环队列来解决。但是在 // 链式队列上则不存在这样的问题 // typedef struct QNode{ struct QNode *next; ElemType elem; }LinkQueue; // 队列需要维护两个 指针 (队头指针,队尾指针) typedef struct{ LinkQueue *head; LinkQueue *tail; int length; }Queue; Status initQueue(Queue *queue);// 带头结点,没有引用传值,就用指针的指针吧 bool isEmpty(Queue *q); int length(Queue *q); // 在头部插入,尾部删除 Status getHead(Queue *q,ElemType *e); Status enQueue(Queue *q,ElemType e); Status deQueue(Queue *q,ElemType *e); void traveser(Queue *q); Status initQueue(Queue *queue){// 带头结点,没有引用传值,就用指针的指针吧 LinkQueue *lq = (LinkQueue *)malloc(sizeof(LinkQueue)); if (!lq) { return ERROR; } lq->elem = 0; lq->next = NULL; (queue)->head = (queue)->tail = lq; // -> 优先级高于 * ,老老实实打上括号 (queue)->length = 0; return OK; } bool isEmpty(Queue *q){ return (q->head == q->tail); } int length(Queue *q){ // int ret; // LinkQueue *p = q->head; // while (p != q->tail) { // ret++; // p = p->next; // // } return q->length; } Status getHead(Queue *q,ElemType *e){ LinkQueue *p = q->head->next; if (!p) { *e=0; return ERROR; } *e = p->elem; return OK; } // 入队(加入到队尾) Status enQueue(Queue *q,ElemType e){ LinkQueue *newNode = (LinkQueue *)malloc(sizeof(LinkQueue)); if (!newNode) { return ERROR; } newNode->elem = e; newNode->next = NULL; q->tail->next = newNode; q->tail = newNode; q->length++; return OK; } //出队(队头). 注意,可能只有1个元素,造成队尾指针丢失 Status deQueue(Queue *q,ElemType *e){ LinkQueue *p = q->head->next; if (!p) {// 队列空 return ERROR; } if (e) { *e = p->elem ; } LinkQueue *temp = p->next; q->head->next = temp; if (p == q->tail) { q->tail = q->head; } free(p); q->length--; return OK; } void traveser(Queue *q){ LinkQueue *p = q->head->next; while (NULL != p) { printf("%d\n",p->elem); p=p->next; } } #endif ================================================ FILE: 4 Tree/1-二叉树 /btree.c ================================================ /* ### B树基础 B树即B-tree(B树就是B-tree),B是balanced,也就是平衡的意思。 B树又叫平衡多路查找树,节点的直接点个数可以多于2个,一颗m阶的B树: 1) 树中每个节点做多含有m个子节点 2) 根节点不是叶子节点,至少有2个孩子 3) 所有叶子节点在同一层 4) 除根节点和叶子节点外,每个分支节点至少有[m/2]个子树 5) 有j个孩子的非叶子节点有 j-1 个关键码,关键码递增一次排列 ### B树应用 数据库 文件系统 ### B树存储结构 ### B树问题和延伸阅读 */ ================================================ FILE: 4 Tree/1-二叉树 /rbtree.c ================================================ /* ### 红黑树基础 一种自平衡的二叉查找树。在每个节点中增加一个存储位表示节点的颜色,可以是red或black 红黑树比AVL树优秀在哪? 5个性质 性质1. 节点是红色或黑色。 性质2. 根是黑色。 性质3. 所有叶子都是黑色(叶子是NIL节点)。 性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点) 性质5. 从任一节点到其每个叶子的所有简单路径 都包含相同数目的黑色节点。 ### 红黑树应用 ### 存储结构 ### 问题与延伸阅读 */ typedef int ElemType typedef struct node{ int color; ElemType key; struct node *lChild,*rChild,*pChild; }*RBTree; int rbtree_insert(RBTree *tree,ElemType key); int rbtree_remove(RBTree *tree,ElemType key); int rbtree_search(RBTree *tree,ElemType key); ================================================ FILE: 4 Tree/1-二叉树 /suffix_tree.c ================================================ /* ### 后缀树(suffix tree)基础 又叫后缀trie,与trie最大不同在于:字符串集合由指定的后缀子串组成。 很适合用来操作字符串的子串。 用于字符串的匹配和查询 ### 后缀树应用 从目标串T中判断是否包含模式串P(时间复杂度接近KMP算法); 从目标串T中查找最长的重复子串; 从目标串T1和T2中查找最长公共子串; Ziv-Lampel无损压缩算法; 从目标串T中查找最长的回文子串; ### 存储结构 ### suffix tree 问题与延伸阅读 后缀数组 */ ================================================ FILE: 4 Tree/2-二叉查找树/BiSearchTree/README.md ================================================ ###环境 ###编译 ================================================ FILE: 4 Tree/2-二叉查找树/BiSearchTree/bisearchtree.c ================================================ // // bisearchtree.c // BiSearchTree // // Created by nonstriater on 14-2-22. // // #include "bisearchtree.h" #include #include /** * 创建二叉查找树 * * @return 指向一颗空树的指针 */ BiSearchTree *bisearch_tree_new(){ return NULL; } /** * 插入节点 * * @param tree tree * @param node 节点值 */ BiSearchTree *bisearch_tree_insert(BiSearchTree *tree,ElemType node){ BiSearchTree *t = tree; BiSearchTree *parent = NULL; BiSearchTree *newNode = (BiSearchTree *)malloc(sizeof(BiSearchTree)); if (NULL==newNode) { printf("malloc failure\n"); return tree; } newNode->key = node; newNode->lChild = NULL; newNode->rChild = NULL; if (NULL==t) { tree = newNode; printf("insert first node %d \n",node); return tree; } // 找合适的位置 while (t!=NULL) { if( node < t->key){ parent = t; t=t->lChild; }else if(node == t->key){ printf("insert ignore:the bi search tree has the node with %d\n",node); break; }else{ parent = t; t=t->rChild; } } if (parent!=NULL) { if (node>parent->key) { printf("insert node %d to right of %d\n",node,parent->key); parent->rChild = newNode; }else{ printf("insert node %d to left of %d\n",node,parent->key); parent->lChild = newNode; } } return tree; } /** * 查找节点 * * @param tree tree * @param node 节点值 * (也可以使用递归方式实现) */ int bisearch_tree_search(BiSearchTree *tree,ElemType node){ BiSearchTree *t; t= tree; while (NULL!=t) { if (nodekey) { t=t->lChild; } else if (node==t->key) { break; }else{ t=t->rChild; } } if (NULL==t) { return -1; } return 0; } //private int _delete_node(BiSearchTree *node,BiSearchTree*parent){ //该节点为叶子节点,直接删除 if (!node->rChild && !node->lChild) { free(node);//父节点???不然野指针,造成崩溃 } else if(!node->rChild){ //右子树空则只需重接它的左子树 BiSearchTree *target=node->lChild; node->key = node->lChild->key; node->lChild=node->lChild->lChild; node->rChild=node->lChild->rChild; free(target); } else if(!node->lChild){ //左子树空只需重接它的右子树 BiSearchTree *target=node->rChild; node->key = node->rChild->key; node->lChild=node->rChild->lChild; node->rChild=node->rChild->rChild; free(target); } else{ //左右子树均不空 BiSearchTree *parent=node,*target=node->lChild; while (target->rChild) { parent = target; target=target->rChild; }// 找到左子树最大的,是删除节点的直接“前驱” node->key = target->key; if (target!=node) { parent->rChild = target->lChild; }else{ node->lChild = target->lChild; } free(target); } return 0; } //删除节点,需要重建排序树 /* 1) 删除节点是叶子节点(分支为0),结构不破坏 2)删除节点只有一个分支(分支为1),结构也不破坏 3)删除节点有2个分支,此时删除节点 思路一: 选左子树的最大节点,或右子树最小节点替换 */ int bisearch_tree_delete(BiSearchTree **tree,ElemType node){ if (NULL==tree) { return -1; } // 查找删除目标节点 BiSearchTree *target=*tree,*parent=NULL; while (NULL!=target) { if (nodekey) { parent=target; target=target->lChild; }else if(node==target->key){ break; }else{ parent=target; target=target->rChild; } } if (NULL==target) { printf("树为空,或想要删除的节点不存在\n"); return -1; } //该节点为叶子节点,直接删除 if (!target->rChild && !target->lChild) { if (NULL==parent) {////只有一个节点的二叉查找树 *tree=NULL; }else{ if (target->key>parent->key) { parent->rChild=NULL; }else{ parent->lChild=NULL; } } free(target);//父节点处理,不然野指针,造成崩溃 } else if(!target->rChild){ //右子树空则只需重接它的左子树 BiSearchTree *del=target->lChild; target->key = target->lChild->key; target->lChild=target->lChild->lChild; target->rChild=target->lChild->rChild; free(del); } else if(!target->lChild){ //左子树空只需重接它的右子树 BiSearchTree *del=target->rChild; target->key = target->rChild->key; target->lChild=target->rChild->lChild; target->rChild=target->rChild->rChild; free(del); } else{ //左右子树均不空 BiSearchTree *p=target,*t=target->lChild; while (t->rChild) { p = t; t=t->rChild; }// 找到左子树最大的,是删除节点的直接“前驱” target->key = t->key; if (p!=target) { p->rChild = t->lChild; }else{ target->lChild = t->lChild; } free(t); } return 0; // if (node==tree->key) { // return _delete_node(tree); // }else if(node>tree->key){ // return bisearch_tree_delete(tree->rChild,node); // }else{ // return bisearch_tree_delete(tree->lChild,node); // } /* // 查找删除目标节点 BiSearchTree *t=tree,*parent=NULL; while (NULL!=t) { if (nodekey) { parent=t; t=t->lChild; }else if(node==t->key){ break; }else{ parent=t; t=t->rChild; } } if (NULL==t) { printf("树为空,或想要删除的节点不存在\n"); return -1; } // parent 可能为null //删除节点0个分支 if (!t->lChild && !t->rChild) { if (t->key>parent->key) { parent->rChild=NULL; }else{ parent->lChild=NULL; } free(t); return 0; } //删除节点有1个分支 else if (t->lChild && !t->rChild) { if (t->key>parent->key) { parent->rChild=t->lChild; }else{ parent->lChild=t->lChild; } free(t); return 0; } else if (!t->lChild && t->rChild) { if (t->key>parent->key) { parent->rChild=t->rChild; }else{ parent->lChild=t->rChild; } free(t); return 0; } //删除节点有2个分支 else if (t->lChild && t->rChild) { if (t->key->parent->key) { } } return -1; */ } // 中序遍历节点,也就是从小到大输出 void bisearch_tree_inorder_traversal(BiSearchTree *tree){ if (tree) { bisearch_tree_inorder_traversal(tree->lChild); printf("%d \n",tree->key); bisearch_tree_inorder_traversal(tree->rChild); } } ================================================ FILE: 4 Tree/2-二叉查找树/BiSearchTree/bisearchtree.h ================================================ // // bisearchtree.h // BiSearchTree // // Created by nonstriater on 14-2-22. // // /** * 二叉查找树(Binary search tree),也叫有序二叉树(Ordered binary tree),排序二叉树(Sorted binary tree)。是指一个空树或者具有下列性质的二叉树: 1. 若任意节点的左子树不为空,则左子树上所有的节点值小于它的根节点值 2. 若任意节点的右子树不为空,则右子树上所有节点的值均大于它的根节点的值 3. 任意节点左右子树也为二叉查找树 4. 没有键值相等的节点 */ #ifndef BiSearchTree_bisearchtree_h #define BiSearchTree_bisearchtree_h typedef int ElemType; typedef struct BiSearchTree{ ElemType key; struct BiSearchTree *lChild; struct BiSearchTree *rChild; }BiSearchTree; /** * 创建二叉查找树 * * @return 指向一颗空树的指针 */ BiSearchTree *bisearch_tree_new(); /** * 插入节点 * * @param tree tree * @param node 节点值 */ BiSearchTree *bisearch_tree_insert(BiSearchTree *tree,ElemType node); /** * 查找节点 * * @param tree tree * @param node 节点值 * @return -1失败 0 成功 */ int bisearch_tree_search(BiSearchTree *tree,ElemType node); //删除节点 int bisearch_tree_delete(BiSearchTree **tree,ElemType node); // 遍历节点 void bisearch_tree_inorder_traversal(BiSearchTree *tree); #endif ================================================ FILE: 4 Tree/2-二叉查找树/BiSearchTree/main.c ================================================ // // main.c // BiSearchTree // // Created by nonstriater on 14-2-22. // // #include #include "bisearchtree.h" int main(){ printf("begining...\n"); BiSearchTree *searchTree; searchTree = bisearch_tree_new(); searchTree = bisearch_tree_insert(searchTree,19);// 第一次insert bisearch_tree_insert(searchTree,3); bisearch_tree_inorder_traversal(searchTree); bisearch_tree_insert(searchTree,122); bisearch_tree_insert(searchTree,55); bisearch_tree_insert(searchTree,65); bisearch_tree_insert(searchTree,180); printf("inorder traversal\n"); bisearch_tree_inorder_traversal(searchTree); //=======search ============================================ printf("find node...\n"); int find = 55; if(bisearch_tree_search(searchTree,find)<0){ printf("node %d not exits\n",find); }else{ printf("node %d exits\n",find); } find = 33; if(bisearch_tree_search(searchTree,find)<0){ printf("node %d not exits\n",find); }else{ printf("node %d exits\n",find); } //====== delete,会影响到serachTree值,所以应该传递serachTree的指针 ====================================== printf("delete node..\n"); find = 4; if(bisearch_tree_delete(&searchTree,find)<0){ printf("delete node %d failure\n",find); }else{ printf("delete node %d success\n",find); bisearch_tree_inorder_traversal(searchTree); bisearch_tree_insert(searchTree,find); } find = 65;//叶子 if(bisearch_tree_delete(&searchTree,find)<0){ printf("delete node failure\n"); }else{ printf("delete node %d success\n",find); bisearch_tree_inorder_traversal(searchTree); bisearch_tree_insert(searchTree,find); } find = 55;//单分支 if(bisearch_tree_delete(&searchTree,find)<0){ printf("delete node failure\n"); }else{ printf("delete node %d success\n ",find); bisearch_tree_inorder_traversal(searchTree); bisearch_tree_insert(searchTree,find); } find = 122;//双分支 if(bisearch_tree_delete(&searchTree,find)<0){ printf("delete node failure\n"); }else{ printf("delete node %d success\n",find); bisearch_tree_inorder_traversal(searchTree); bisearch_tree_insert(searchTree,find); } return 0; } /* 测试结果: begining... insert first node 19 insert node 3 to left of 19 3 19 insert node 122 to right of 19 insert node 55 to left of 122 insert node 65 to right of 55 insert node 180 to right of 122 inorder traversal 3 19 55 65 122 180 find node... node 55 exits node 33 not exits delete node.. 树为空,或想要删除的节点不存在 delete node 4 failure delete node 65 success 3 19 55 122 180 insert node 65 to right of 55 delete node 55 success 3 19 65 122 180 insert node 55 to left of 65 delete node 122 success 3 19 55 65 180 insert node 122 to left of 180 */ ================================================ FILE: 4 Tree/2-二叉查找树/二叉查找树.md ================================================ # 二叉查找树BST 二叉查找树(Binary search tree),也叫`有序二叉树(Ordered binary tree)`,`排序二叉树(Sorted binary tree)`。 是指一个空树或者具有下列性质的二叉树: 1. 若任意节点的左子树不为空,则左子树上所有的节点值小于它的根节点值 2. 若任意节点的右子树不为空,则右子树上所有节点的值均大于它的根节点的值 3. 任意节点左右子树也为二叉查找树 4. 没有键值(key)相等的节点 有序的二叉查找树,中序遍历结果是递增的。 `左小右大` ``` typedef int ElemType; typedef struct BiSearchTree{ ElemType key; struct BiSearchTree *lChild; struct BiSearchTree *rChild; }BiSearchTree; BiSearchTree *bisearch_tree_insert(BiSearchTree *tree,ElemType node); int bisearch_tree_delete(BiSearchTree **tree,ElemType node); int bisearch_tree_search(BiSearchTree *tree,ElemType node); ``` ### 删除节点 删除节点,需要重建排序树 1) 删除节点是叶子节点(分支为0),结构不破坏 2)删除节点只有一个分支(分支为1),结构也不破坏 3)删除节点有2个分支,此时删除节点 ; 需要重建树 思路一: 选左子树的最大节点,或右子树最小节点替换 ``` int bisearch_tree_delete(BiSearchTree **tree,ElemType node){ if (NULL==tree) { return -1; } // 查找删除目标节点 BiSearchTree *target=*tree,*parent=NULL; while (NULL!=target) { if (nodekey) { parent=target; target=target->lChild; }else if(node==target->key){ break; }else{ parent=target; target=target->rChild; } } if (NULL==target) { printf("树为空,或想要删除的节点不存在\n"); return -1; } //该节点为叶子节点,直接删除 if (!target->rChild && !target->lChild) { if (NULL==parent) {////只有一个节点的二叉查找树 *tree=NULL; }else{ if (target->key>parent->key) { parent->rChild=NULL; }else{ parent->lChild=NULL; } } free(target);//父节点处理,不然野指针,造成崩溃 } else if(!target->rChild){ //右子树空则只需重接它的左子树,用左子树替换掉当前要删除的节点 BiSearchTree *del=target->lChild; target->key = target->lChild->key; target->lChild=target->lChild->lChild; target->rChild=target->lChild->rChild; free(del); } else if(!target->lChild){ //左子树空只需重接它的右子树 BiSearchTree *del=target->rChild; target->key = target->rChild->key; target->lChild=target->rChild->lChild; target->rChild=target->rChild->rChild; free(del); } else{ //左右子树均不空,p,t 2个指针一前以后,将左子树最大的节点(肯定是一个最右的节点)替换到删除的节点后,还需要处理左子树最大节点的左子树 BiSearchTree *p=target,*t=target->lChild; while (t->rChild) { p = t; t=t->rChild; }// 找到左子树最大的,是删除节点的直接“前驱” target->key = t->key; if (p!=target) { p->rChild = t->lChild; }else{ target->lChild = t->lChild; } free(t); } return 0; } ``` ================================================ FILE: 4 Tree/3-平衡树AVL/AVLTree.c ================================================ /* 记于2014-2-28 by @nonstriater */ #include #include #define LH 1 #define EH 0 #define RH -1 typedef int KEY_TYPE; typedef struct node{ KEY_TYPE key; int height; // 平衡因子 struct node *lChild; struct node *rChild; }AVLTree; // void avltree_rr_rotate(AVLTree **tree){ AVLTree *right= *tree->rChild; right->lChild = *tree; *tree->rChild = right->lChild; } // void avltree_ll_rotate(AVLTree **tree){ AVLTree *left = *tree->lChild; left->rChild = *tree; *tree->lChild = left->rChild } // void avltree_lr_rotate(AVLTree **tree){ } void avltree_rl_rotate(AVLTree **tree){ } void avltree_left_balance(AVLTree **root) { AVLTree *left,*lr; left=(*root)->lChild; switch(left->height) { //检查T的左子树平衡度,并作相应的平衡处理 case LH://新节点插入在T的左孩子的左子树上,做单右旋处理 (*root)->height=left->height=EH; avltree_ll_rotate(root); break; case RH://新插入节点在T的左孩子的右子树上,做双旋处理 lr=left->rChild; switch(lr->height) { case LH: (*root)->height=RH; left->height=EH; break; case EH: (*root)->height=left->height=EH; break; case RH: (*root)->height=EH; left->height=LH; break; } lr->height=EH; L_Rotate(&(*T)->lChild); R_Rotate(T); } } void avltree_right_balance(AVLTree **root) { AVLTree right,rl; right=(*root)->rChild; switch(right->height) { case RH://新节点插在T的右孩子的右子树上,要做单左旋处理 (*root)->height=right->height=EH; avltree_rr_rotate(root); break; case LH://新节点插在T的右孩子的左子树上,要做双旋处理 rl=right->lChild; switch(rl->height) { case LH: (*root)->height=EH; right->height=RH; break; case EH: (*root)->height=right->height=EH; break; case RH: (*root)->height=LH; right->height=EH; break; } rl->height=EH; R_Rotate(&(*root)->rChild); L_Rotate(T); } } // 插入一个节点key /* 算法描述: 1)如果root为null,则插入一个数据元素为kx 的新结点作为T 的根结点 2)如果key和root->key相等,不插入 3)如果keykey, 插在root左子树上: */ AVLTree* avltree_insert(AVLTree* root, KEY_TYPE key){ if (NULL==root) { root = (AVLTree *)malloc(sizeof(struct AVLTree)); if (!root) { printf("内存分配失败,插入节点失败\n"); return root; } root.key = key; root.lChild = NULL; root.rChild = NULL; root.height = 0; } else if (key=root->key) { printf("节点 %d 已存在 \n", key); } else if (keykey)//插入左 { root->lChild = avltree_insert(root->lChild,key); if (root->lChild->height-root->rChild->height == 2)//不平衡 { if (keylChild->key)// LL型 { root = avltree_ll_rotate(tree); } if (key>root->lChild->key)//LR { root = avltree_lr_rotate(tree); } } } else if (key>root->key){ root->rChild = avltree_insert(root->rChild,key); if (keyrChild->key)// RL { root = avltree_rl_rotate(tree); } if (key>root->rChild->key)// RR { root = avltree_rr_rotate(tree); } } return root; } // 删除一个节点 AVLTree* avltree_delete(AVLTree* root, KEY_TYPE key){ } // 判断是否为AVL树 int avltree_isbalance(AVLTree *root){ } // 查找 AVLTree* avltree_search(AVLTree *root,KEY_TYPE key){ if (root==NULL) { return NULL; } if (root->key == key) { return root; } else if (root->key>key) { return avltree_search(root->lChild,key); } else{ return avltree_search(root->rChild,key); } } // 中序遍历 void avltree_inorder_traversal(AVLTree* root){ if (root) { avltree_inorder_traversal(root->lChild); printf(节点值=%d,左右子树的高度差=%d\n,root->key,root->height); avltree_inorder_traversal(root->rChild); } } // test int main(){ AVLTree *avlTree=NULL; printf("插入节点,创建一个AVL树...\n"); int values[] = {11,7,222,456,23,8,65,124,88,2,54}; for (int i = 0; i < sizeof(values)/sizeof(int); ++i) { printf("插入节点 %d\n", values[i]); avlTree = avltree_insert(avlTree,values[i]); avltree_inorder_traversal(avltree); printf("\n\n"); } printf("中序遍历结果:\n"); avltree_inorder_traversal(avlTree); printf("删除一个存在的节点 %d\n", values[1]); avlTree=avltree_delete(avlTree,values[1]); printf("中序遍历结果:\n"); avltree_inorder_traversal(avlTree); printf("删除一个不存在的节点 %d\n",111 ); avltree_delete(avlTree,111); printf("中序遍历结果:\n"); avltree_inorder_traversal(avlTree); printf("查找一个存在的节点 %d\n", values[3]); avltree_search(avltree,values[3]); printf("查找一个不存在的节点 %d\n",51); avltree_search(avltree,51); return 0; } ================================================ FILE: 4 Tree/3-平衡树AVL/README.md ================================================ # 自平衡二叉查找树(AVL tree) 自平衡二叉查找树(AVL tree): 首先也是二次查找树,其实 任何2个子树的高度差不大于1 在删除,插入的过程中不断调整子树的高度,保证查找操作平均和最坏情况下都是O(logn) Adelson-Velskii 和 Landis 1962年 创造, 因此叫做 AVL 树。 1) 平衡因子 -1 0 1 节点是正常的。平衡因子 = 左子树高度-右字数高度 2) 除此之外的节点是不平衡的,需要重新平衡这个树。也就是AVL旋转 AVL 实际使用案例 * LLVM 的 ImmutableSet,其底层的实现选择为 AVL 树 * 《一种基于二叉平衡树的P2P覆盖网络的研究》论文 ## 插入节点 a: 左旋转(RR型:节点x的右孩子的右孩子上插入新元素)平衡因子由-1 -》-2 时,需要绕节点x左旋转 b:右旋转(LL型:节点X的左孩子的左孩子上插入新元素) 平衡因子有1-》2,右旋转 c: 先左后右旋转:(LR型:树中节点X的左孩子的右孩子上插入新元素) 平衡因子从1变成2后,就需要 先绕X的左子节点Y左旋转,接着再绕X右旋转 d: 先右后左旋转:(RL型:节点X的右孩子的左孩子上插入新元素) 6 6 6 6 / \ / \ 5 7 3 9 / \ \ / 3 8 5 7 (LL型) (RR) (LR) (RL) ## 删除节点 可以看到,为了保证高度平衡,插入和删除操作代价增加 ## AVL 实现过程中的问题 AVL 是严格的平衡二叉树,平衡条件必须满足,即所有节点的左右子树高度差的绝对值不超过1; 执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的,由此我们可以知道AVL树适合用于插入与删除次数比较少,但查找多的情况。 由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部平衡而不是非常严格整体平衡的红黑树()。 ================================================ FILE: 4 Tree/4-字典树Trie/README.md ================================================ # 字典树trie `字典树`,英文名`Trie树`,Trie一词来自retrieve,发音为/tri:/ “tree”,也有人读为/traɪ/ “try”, 又称`单词查找树` 或 `前缀树`,Trie树,是一种树形结构(多叉树)。 trie,又称为前缀树或字典树,是一种有序树,用于保存关联数组。 1. 除根节点不包含字符,每个节点都包含一个字符 2. 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串 3. 每个节点的所有子节点包含的字符都不相同(保证每个节点对应的字符串都不一样) 比如: ``` / \ / | \ t a i / \ \ o e n /|\ / a d n n ``` 上面的Trie树,可以表示字符串集合{“a”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”} 。 trie树把每个关键字保存在一条路径上,而不是一个节点中 两个有公共前缀的关键字,在Trie树中前缀部分的路径相同,所以Trie树又叫做前缀树(Prefix Tree)。 ### trie 优缺点 它的优点是: 1. 插入和查询的效率很高,都是O(m),其中 m 是待插入/查询的字符串的长度 2. Trie树可以对关键字按字典序排序 3. 利用字符串的公共前缀来最大限度地减少无谓的字符串比较,提高查询效率 缺点: 1. trie 树比较费内存空间,在处理大数据时会内存吃紧 2. 当hash函数较好时,Hash查询效率比 trie 更优 [知乎这里](http://www.zhihu.com/question/27168319)有个问题:`10万个串找给定的串是否存在`, 对trie和hash两种方案给出了讨论。 [DATrie](https://github.com/kmike/datrie) 是使用python实现的双数组trie树, 双数组可以减少内存的使用量 。有关 double-array trie,可以参考[这篇论文](http://linux.thai.net/~thep/datrie/datrie.html) ### trie应用 典型应用是:前缀查询,字符串查询,排序 * 用于统计,排序和保存大量的字符串(但不仅限于字符串) * 经常被搜索引擎系统用于文本词频统计 * 排序大量字符串 * 用于索引结构 * 敏感词过滤 ### 实际应用问题 1. 给你100000个长度不超过10的单词。对于每一个单词,我们要判断他出没出现过,如果出现了,求第一次出现在第几个位置 分析思路一:trie树 ,找到这个字符串查询操作就可以了,如何知道出现的第一个位置呢?我们可以在trie树中加一个字段来记录当前字符串第一次出现的位置。 2. 已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个串为另一个串的前缀子串 3. 给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。 分析:trie树查询单词的应用。先建立N个熟词的前缀树,然后按文章的单词一次查询。 4. 给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。 分析:先用不良单词建立trie树,然后过滤文本(每个单词都在trie树上查询,查询的复杂度O(1),效率非常高),这正是`敏感词过滤系统(或垃圾评论系统)`的原理。 5. 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出 分析:这是trie树排序的典型应用,建立N个单词的trie树,然后线序遍历整个树,就可以达到效果。 ## trie树存储结构和基本操作 最简单实现 ---- 26个字母表 `a-z` (没有考虑数字,大小写,其他字符如 `=-*/`) trie 树存储结构 * 用数组存储,浪费空间;如果系统中存在大量字符串,且这些字符串基本没有公共前缀,trie树将消耗大量内存 * 用链表存储,查询时需要遍历链表,查询效率有所降低 ``` define ALPHABET_NUM 26 typedef struct trie_node{ char value; bool isKey;/*是否代表一个关键字*/ int count; /*可用于词频统计,表示关键字出现的次数*/ struct Node *subTries[ALPHABET]; }*Trie Trie Trie_create(); int Trie_insert(Trie trie,char *word); // 插入一个单词 int Trie_search(Trie trie,char *word);// 查找一个单词 int Trie_delete(Trie trie,char *word);// 删除一个单词 Trie Trie_create(){ trie_node* pNode = new trie_node(); pNode->count = 0; for(int i=0; ichildren[i] = NULL; return pNode; } void trie_insert(trie root, char* key) { trie_node* node = root; char* p = key; while(*p) { if(node->children[*p-'a'] == NULL) { node->children[*p-'a'] = create_trie_node(); } node = node->children[*p-'a']; ++p; } node->count += 1; } /** * 查询:不存在返回0,存在返回出现的次数 */ int trie_search(trie root, char* key) { trie_node* node = root; char* p = key; while(*p && node!=NULL) { node = node->children[*p-'a']; ++p; } if(node == NULL) return 0; else return node->count; } ``` trie树的增加和删除都比较麻烦,但索引本身就是写少读多,是否考虑添加删除的复杂度上升,依靠具体场景决定。 ================================================ FILE: 4 Tree/4-字典树Trie/trie.c ================================================ /* trie树 */ #include #include #define ALPHABET_SIZE 26 // 256 typedef int bool; #define YES 1 #define NO 0 typedef struct node { int count; //count 如果为0,则代表非黄色点,count>0代表是黄色点,同时表示出现次数; char value; //字符 struct node * subtries[ALPHABET_SIZE]; //子树 } Trie; Trie *trie_create(Trie **trie){ if (NULL==*trie) { *trie = (Trie *)malloc(sizeof(struct node)); if (NULL==*trie) { printf("malloc failure...\n"); } for (int i = 0; i < ALPHABET_SIZE; ++i) { (*trie)->subtries[i]=NULL; } } return *trie; } //插入字符串(插入一个单词),建立字典树. 返回值 < 0 表示插入失败 int trie_insert(Trie *trie,char *c){ if (trie==NULL || c==NULL) // 对NULL指针解引用会崩溃 { return -1; } char *p = c; Trie *temp = trie; while(*p != '\0'){ if (temp->subtries==NULL) { //temp->subtries = (struct node *)malloc(sizeof(struct node)*ALPHABET_SIZE); if (temp->subtries==NULL) { return -1; } } if (temp->subtries[*p-'a']==NULL) { struct node *newNode = (struct node *)malloc(sizeof(struct node)); if (!newNode) { printf("create new node fail \n"); return -1; } newNode->value = *p; //newNode->subtries = NULL; temp->subtries[*p-'a'] = newNode; } temp = temp->subtries[*p-'a']; p++; } return 0; } // 字符串查找,返回值<0表示没有查找到 bool trie_query(Trie *trie,char *c){ if (trie==NULL ) { return NO; } char *p = c; Trie *temp = trie; bool ret=NO; if (temp->subtries == NULL) { return NO; } while (*p!='\0') { if (temp->subtries[*p-'a']!=NULL && temp->subtries[*p-'a']->value==*p)//匹配 { temp = temp->subtries[*p-'a']; p++; continue; } break; } if (*p =='\0') { ret = YES; } return ret; } // void trie_remove(){} int main(){ Trie *trie = NULL; if (!trie_create(&trie)) { printf("trie init fail...\n"); } char *dict[10]={"int","integer","float","char","nonstriater","weibo"}; for (int i = 0; i < 6; ++i) { if (trie_insert(trie,dict[i])<0) { printf("%s 插入失败\n", dict[i]); } } // 查询cha printf("查询cha \n"); if (trie_query(trie,"cha")) { printf("YES\n"); }else{ printf("NO\n"); } // 查询char printf("查询char \n"); if (trie_query(trie,"char")) { printf("YES\n"); }else{ printf("NO\n"); } // 查询hello printf("查询hello \n"); if (trie_query(trie,"hello")) { printf("YES\n"); }else{ printf("NO\n"); } return 0; } ================================================ FILE: 4 Tree/5-伸展树/伸展树.md ================================================ # 伸展树 (splay tree) 伸展树是一种自平衡的二叉排序树。为什么需要这些自平衡的二叉排序树? n个节点的完全二叉树,其查找,删除的复杂度都是O(logN),但是如果频繁的插入删除,导致二叉树退化成一个n个节点的单链表,也就是`插入,查找复杂度趋于O(N)`,为了克服这个缺点,出现了很多二叉查找树的变形,如AVL树,红黑树,以及接下来介绍的 伸展树(splay tree)。 ================================================ FILE: 4 Tree/6-后缀树/后缀树.md ================================================ # 后缀树(suffix tree) ###后缀树的应用 可以解决很多字符串的问题 1. 查找字符串S1是否在字符串S中 2. 指定字符串S1在字符串S中出现的次数 3. 字符串S中的最长重复子串 4. 2个字符串的最长公共部分 ================================================ FILE: 4 Tree/7-B树/B+树.md ================================================ # B+树 ================================================ FILE: 4 Tree/7-B树/B树.md ================================================ # B树 一种多路平衡查找树。 能保证数据插入和删除情况下,任然保持执行效率。 一个M阶的B树满足: 1. 每个节点最多M个子节点 2. 除跟节点和叶节点外,其它每个节点至少有M/2个孩子 3. 根节点至少2个节点 4. 所有叶节点在同一层,叶节点不包含任何关键字信息 5. 有k个关键字的页节点包含k+1个孩子 也就是说:`根节点到每个叶节点的路径长度都是相同的。` ## 数据结构 ``` typedef struct Item{ int key; Data data; } #define m 3 //B树的阶 typedef struct BTNode{ int degree; //B树的度 int keynums; //每个节点key的个数 Item items[m]; struct BTNode *p[m]; }BTNode,* BTree; typedef struct{ BTNode *pt; //指向找到的节点 int i; // 节点中关键字的序号 (0,m-1) int tag; //1:查找成功,0:查找失败 }Result; Status btree_insert(root,target)//插入B树节点 Result btree_find(root,target)//查找B树节点 Status btree_delete(root,target)//删除B树节点 ``` ## 插入B树节点 ## 查找B树节点 ## 删除B树节点 ================================================ FILE: 4 Tree/8-堆/Top-K 问题.md ================================================ # Top-K 问题 问题描述:从arr[1, n]这n个数中,找出最大的k个数,这就是经典的TopK问题 思路 1. 排序,全局都排序了,这也是这个方法复杂度非常高的原因 ; [排序算法参考这里](../../6%20Sort/REAME.md) 2. 冒泡排序,局部排序徐,只对最大的k个数排序 3. [堆排序](./堆.md), 只找最大的k个数,这k个数不需要排序; top-k大 问题就是用 小根堆, 小根堆 固定为 k 个元素大小 , 遍历 k-N (N为所有数据的个数),插入小根堆并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。 ================================================ FILE: 4 Tree/8-堆/heap.c ================================================ void heap_build(int *a,int length){ } // 插入 void heap_insert(int *a,int v){ } //删除,删除的元素放在value指向的内存中 void heap_delete(int *a,int *value){ } ================================================ FILE: 4 Tree/8-堆/堆.md ================================================ # 大顶堆 和 小顶堆 先来了解下 `堆` 结构; 堆也被称为`优先队列`,`二叉堆` ; 特点 * 堆总是一颗完全二叉树树, 使用数组作为其存储结构,因此也叫 `二叉堆`; 用链表存的就叫二叉树了 * 任一节点小于(或大于)其所有的孩子节点; * 堆分小根堆和大根堆; 如果根节点大于所有孩子节点,这就是一颗大根堆,也就是根节点是堆上的最大值;如果节点小于所有的子节点,这就是一颗小跟堆,也即是根节点是堆上所有节点的最小值。 * 小根堆: 每次取出来的元素都是队列中值最小的 `堆用数组来存储`,因为是一颗完全二叉树,i节点的父节点索引就是(i-1)/2, 左右子节点小标是 2i+1,2i+2。 比如小根堆存储示例 ![小根堆](./pq-1.jpg) ### 应用场景 * 优先队列 如iOS中的 NSOperationQueue 就是维护一个优先队列 * 堆排序 * [top-K 大(小)](Top-K%20问题.md) , top-k大 问题就是用 小根堆, 小根堆 固定为 k 个元素大小 , 遍历 k-N (N为所有数据的个数),插入小根堆并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。 ### Java PriorityQueue Java PriorityQueue 类就是 通过二叉小顶堆实现 ,也叫优先级队列实现。 有如下特点: * 实现 Queue 接口 * 头部是基于自然排序或基于比较器的排序的最小元素 * 不是线程安全的,PriorityBlockingQueue在并发环境中使用 API 如下: * boolean add(object):将指定的元素插入此优先级队列。 * boolean offer(object):将指定的元素插入此优先级队列。 * boolean remove(object):从此队列中删除指定元素的单个实例(如果存在)。 * Object poll():检索并删除此队列的*头部*,如果此队列为空,则返回null。 * Object element():检索但不删除此队列的*头部*,如果此队列为空,则返回null。 * Object peek():检索但不删除此队列的*头部*,如果此队列为空,则返回null。 * void clear():从此优先级队列中删除所有元素。 ``` { PriorityQueue pq = new PriorityQueue(); pq.add(new Employee(1L, "AAA", LocalDate.now())); pq.add(new Employee(4L, "BBB", LocalDate.now())); pq.add(new Employee(3L, "DDD", LocalDate.now())); pq.add(new Employee(7L, "GGG", LocalDate.now())); pq.add(new Employee(2L, "CCC", LocalDate.now())); while (true) { Employee head = pq.poll(); System.out.println(head); if (head == null) { return; } } } ``` 输出 ``` Employee [id=1, name=AAA, dob=2021-12-22] Employee [id=2, name=CCC, dob=2021-12-22] Employee [id=3, name=DDD, dob=2021-12-22] Employee [id=4, name=BBB, dob=2021-12-22] Employee [id=7, name=GGG, dob=2021-12-22] ``` 如下,使用 PriorityQueue实现的 top-K 大问题,实现如下: ```Java /** * 小根堆实现 */ public static int findMaxK(int[] nums, int k) { PriorityQueue pq = new PriorityQueue<>(k, (a, b) -> (a-b) ); for (int i = 0; i > { // 存储元素的数组 private Key[] pq; // 当前 Priority Queue 中的元素个数 private int N = 0; public MaxPQ(int cap) { // 索引 0 不用,所以多分配一个空间 pq = (Key[]) new Comparable[cap + 1]; } /* 返回当前队列中最大元素 */ public Key max() { return pq[1]; } /* 插入元素 e */ public void insert(Key e) {...} /* 删除并返回当前队列中最大元素 */ public Key delMax() {...} /* 上浮第 k 个元素,以维护最大堆性质 */ private void swim(int k) {...} /* 下沉第 k 个元素,以维护最大堆性质 */ private void sink(int k) {...} /* 交换数组的两个元素 */ private void exch(int i, int j) { Key temp = pq[i]; pq[i] = pq[j]; pq[j] = temp; } /* pq[i] 是否比 pq[j] 小? */ private boolean less(int i, int j) { return pq[i].compareTo(pq[j]) < 0; } /* 还有 left, right, parent 三个方法 */ } ``` ================================================ FILE: 4 Tree/9-红黑树 R-B tree/红黑树.md ================================================ # 红黑树 (red-black tree) 红黑树(Red Black Tree) 是一种自平衡二叉查找树。一种特化的[AVL树](../3-平衡树AVL/README.md),在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。 它可以在O(log n)时间内做查找,插入和删除。 它的每个结点都被“着色”为红色或者黑色,这些结点的颜色被用来检测树的平衡性。 ### 应用场景 * C++ STL的map和set * java 中 HashMap、TreeMap 的底层实现,当HashMap中元素大于8个时,HashMap底层存储实现改为红黑树,以提高元素搜索速度。 关于 HashMap 实现解析参考 [这里](../../3%20HashTable/HashMap%20in%20Java.md) * 广泛应用Linux 的进程管理、内存管理,设备驱动及虚拟内存跟踪 * epoll的的的实现采用红黑树组织管理的的的sockfd,以支持快速的增删改查 * Nginx的的的中用红黑树管理定时器,因为红黑树是有序的,可以很快的得到距离当前最小的定时器 ### RB tree 特点 * 每个节点非红即黑 * 根节点是黑的 * 每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的 * 如果一个节点是红的,那么它的两儿子都是黑的 * 对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点 * 每条路径都包含相同的黑节点 在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑);通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍; 因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度低于红黑树)。相对于要求严格的[AVL树](../3-平衡树AVL/README.md)来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树。 也就是说,红黑树牺牲掉一定的平衡性(牺牲查找性能),换来了 插入,删除操作时 更少的旋转次数带来的开销。 ### 红黑树 & B+ 树对比 * 红黑树多用在内部排序,即全放在内存中的 * B+树多用于外存上时,B+也被成为一个磁盘友好的数据结构; 这也是为什么 mysql索引使用b+树而不使用红黑树 为什么使用 红黑树 而不是 B+ 树呢?原因如下: * 没有范围查找, 不需要 B+ * 不需要多路平衡树,使用二路平衡,实现简单,且红黑树能兼顾 查找,删除操作的性能 ================================================ FILE: 4 Tree/92-并查集/并查集.md ================================================ # 并查集 ================================================ FILE: 4 Tree/README.md ================================================ # 树🌲 介绍树相关的算法 * 二叉树 * 二叉查找树 * AVL树 * 红黑树 * B树 : B树, B+树(mysql索引使用B+树的数据结构) * 字典树trie(前缀树,单词查找树) * 伸展树 * 后缀树 * 红黑树 * 二叉堆(优先队列) * Treap 树 * 赫夫曼编码 Huffman ## 二叉树 [快速排序](../6%20Sort/README.md)就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历 ## [二叉查找树BST](2-二叉查找树/二叉查找树.md) 有序的二叉树,中序遍历结果是递增的 ## [AVL树](3-平衡树AVL/README.md) 绝对的平衡二叉树; ## [红黑树](9-红黑树%20R-B%20tree/红黑树.md) 弱平衡二叉树;使用广泛 ## [字典树trie](4-字典树Trie/README.md) 字典树也叫前缀树,单词查找树 ## [伸展树](5-伸展树/伸展树.md) ## [后缀树](6-后缀树/后缀树.md) ## B树 * [B树](7-B树/B树.md) * [B+树](7-B树/B+树.md) mysql 索引使用 B+树 的数据结构 ## [二叉堆](8-堆/堆.md) ================================================ FILE: 4 Tree/huffman tree/赫夫曼编码.md ================================================ # 赫夫曼编码 Huffman Huffman在1952年根据香农(Shannon)在1948年和范若(Fano)在1949年阐述的这种编码思想提出了一种不定长编码的方法,也称霍夫曼(Huffman)编码。 这是一个经典的压缩算法。通过`字符出现的频率`,`优先级`,`二叉树` 进行的压缩算法。 对一个字符串,计算每个字符出现的次数, 把这些字符放到优先队列(priority queue); 这个priority queue转出二叉树 需要一个字符编码表来解码,通过二叉树建立huffman编码和解码的字典表 举一个例子: 原始串: 二级制编码: huffman编码: ### 存储结构和基本操作 ``` struct node{ char *huffCode; // 叶子节点的huff编码 int weight; struct node *left,right; } ``` ### 构建赫夫曼树 原则:出现频率越多的会在越上层,编码也越短,出现频率越少的在越下层,编码也越长。 不存在某一个编码是另一个编码的前缀,字符都在叶节点上,所以不会存在一个编码是另一个编码的前缀 二叉树每个节点要么是叶子节点,要么是双分支节点(且左分支编码为0,右分支编码为1) ### 压缩 1. 扫描输入文件,统计各个字符出现的次数,对结构排序 (hash统计每个字符出现的次数) 2. 根据排序结构,构建赫夫曼树 (贪心策略,每次选频率值最低的2个节点合并,需要优先队列帮组(priority queue,又叫最小堆)) 3. 对树进行遍历(左分支编码为0,右分支编码为1),得到各个字符的huffman编码,存到hash表中(这个就是编解码表,也可直接存储到节点中,如上面的char *huffCode) 4. 重新对文件扫描,根据hash表进行压缩 压缩的文件为了能够解压缩,需要一个文件头,用来重建赫夫曼树,包括: 被编码的文本长度 unsigned int size 字符频率表 unsigned char freqs[NUM_CHARS] ###解压缩 1. 读取文件头 2. 遍历编码后的bits,从赫夫曼树的根节点出发,遇到0,进入左子树,遇到1进入右子树,直到叶节点 ================================================ FILE: 5 Graph/DFS 和 BFS.md ================================================ # DFS 和 BFS 搜索算法 DFS: 深度优先搜索,以深度为准则,先一条路走到底,直到达到目标; 没有达到目标又无路可走了,那么则退回到上一步的状态,走其他路。这便是回溯上来。 BFS:广度优先搜素,在面临一个路口时,把所有的岔路口都记下来,然后选择其中一个进入,然后将它的分路情况记录下来,然后再返回来进入另外一个岔路,并重复这样的操作。 > DFS用递归的形式,用到了栈结构,先进后出; BFS选取状态用队列的形式,先进先出。 ================================================ FILE: 5 Graph/README.md ================================================ # 图 ### 什么是图 图论(Graph theory)是数学的一个分支,它以图为研究对象,研究顶点和边组成的图形的数学理论和方法。图论中的图是由若干给定的顶点及连接两顶点的边所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用顶点代表事物,用连接两顶点的边表示相应两个事物间具有这种关系。 图论的研究对象相当于一维的拓扑学。 ### 应用场景 工业界有哪些应用场景? * 匹配,如打车中司乘匹配引擎,如何做到效率最优 * 并行任务调度: 一组任务,任务有优先级,如何合理安排任务调度,在最短时间内完成 * 导航路径规划:在使用导航软件时,用户在选择一个开始地点和目的地之后导航软件会给出各种如路程最短,不走高速,时长最短等方案 * 社区发现: 在好友关系中,根据社区之间联系或紧密,利用图 louvain 算法或者其他算法对用户进行分群从而达到精准营销,个性化服务等 * 金融贷后催收:利用图算法找出符合条件的失联人的联系人,从而提高催收失联修复的覆盖率、有效联系率,助力不良资产的回收 * 套汇 ### 基本概念 ** 有向图 ** 有向图,就是有方向的图 ** 无向图 ** 就是没有方向的图 ** 环 ** 首尾相接的路径我们就把它叫做一个环。 # 图的存储结构 * 对象和指针 * 邻接矩阵 (二维数组) * 邻接表 图数据结构表示: ``` ``` ## 图的操作 #### 遍历 * 广度优先 BFS * 深度优先 DFS Dijkstra A* 用于游戏编程和分布式计算 ## 图延伸 * 二部图 * 有向无环图 DAG ### 参考 https://www.jiqizhixin.com/articles/2019-05-16-14 ================================================ FILE: 5 Graph/二分图/二部图.md ================================================ # 二部图 ================================================ FILE: 5 Graph/拓扑排序.md ================================================ # 拓扑排序 ================================================ FILE: 5 Graph/最小生成树.md ================================================ # 最小生成树 ================================================ FILE: 5 Graph/最短路径.md ================================================ # 最短路径算法 最短路径问题: 寻找图(由结点和路径组成的)中两结点之间的最短路径 实现算法 * A* 算法 * Floyd * Dijkstra(迪杰斯特拉) * bellman-ford * spfa ## A* 算法 `A*(A-Star)`算法是一种静态路网中求解最短路径最有效的直接搜索方法; 算法中的距离估算值与实际值越接近,最终搜索速度越快。 ## Dijkstra:最短路径算法 Dijkstra 解决图中一点到其余各点到最短路径的问题 Dijkstra是荷兰的计算机科学家,提出”信号量和PV原语“,"解决哲学家就餐问题",”死锁“也是它提出来的) ================================================ FILE: 6 Sort/8.c ================================================ #include "stdio.h" int main(int argc, char const *argv[]) { int length=10; int j; for (int i = 0; i < length; ++i) { for (j = i+1 ; j < length; ++j) { printf("i=%d,j=%d\n",i,j ); } } for (int i = 0; i < length; ++i) { for (int k = i+1 ; k < length; ++k) { printf("i=%d,k=%d\n",i,k ); } } return 0; } ================================================ FILE: 6 Sort/README.md ================================================ # 排序算法 **排序的稳定性** 是指对于相等的元素,排序之后,任然保存2个元素的位置没有变,就是稳定的排序,反之就是不稳定排序。 * 交换排序算法 * 桶排序 ### 交换排序算法 * 冒泡排序 :没轮确定一个最大的数排后面 * 选择排序 :每轮选择最小的数排前面 * 快排 : 选最后一个作为pivot(基数), 将数据分为 左边小于 pivot, 右边大于 pivot; 分治递归, 二叉树的前序遍历思路 * 归并排序 : 将无序的数组,分成2个子数组分别排序,然后再merge ; 分治递归, 二叉树的后续遍历思路 * 插入排序 : * 希尔排序 : * [堆排序](../4%20Tree/8-堆/堆.md) : ### 线性排序算法 * 桶排序 ### 小结 常见的排序算法都是比较排序,比较排序的时间复杂度通常为 O(n^2) 或 O(nlogn) 但是如果带排序的数字有一些特俗性时,我们可以根据这来设计更加优化的排序算法。 # 交换排序算法 排序算法的复杂度由 `比较的次数` 和 `交换的次数` 一起决定。 ### 直接选择排序 1. 从未排序的序列中选择最小的元素,与放在第一个位置的元素交换 2. 依次类推,直到全部排序 在a【i,n】中最小的元素和 a[i]交换位置。空间复杂度O(1),时间复杂度 O(n^2) ![](./selectsort.gif) ### 冒泡排序 1. 相邻的2各元素比较,大的向后移,经过一轮比较,做大的元素排在最后 2. 第二轮,第二大的元素排倒数第二个位置 3. 直到全部排好 这样,即使是排好序的拿冒泡排序排序,比较的时间复杂度O(n^2) ![](./bubblesort.gif) ### 快速排序 时间复杂度 平均复杂度 O(n * logn) , 最坏 O(n^2) 空间复杂度 快速排序是对冒泡排序的改进,划分交换排序。 递归一次,pivot 左边都比它小,右边都比它大。这是递归,分治的思想。 > 快速排序就是个二叉树的前序遍历思路,归并排序就是个二叉树的后序遍历思路 代码框架如下 ```C void quicksort(int *a, int left, int right){ if (left=tmp){ j--; } if (i0; --i) { //交换 heap_swop(&a[0],&a[i]); //调整 heap_adjust(a,i); } } 推排序还可以用来求 top-K 大(小)的问题。 #排序动画演示 接下来,我们看一组排序的动画,你看看能不能猜到他们使用了什么排序算法完成。 ![](./qsort.gif) ![](./mergesort.gif) ![](./heapsort.gif) ![](./selectsort.gif) ![](./bubblesort.gif) ![](./shellsort.gif) 答案是: 快排 |归并排序|堆排序 选择排序|冒泡排序|希尔排序 国外也有人通过舞蹈的方式编排了几种基本的排序算法,非常有趣。[点这里去看看](http://v.youku.com/v_show/id_XMzMyODk4NTQ4.html?from=s1.8-1-1.2) # 线性排序算法 上面的算法都是基于比较的排序,时间复杂度最好也是 NlogN. 而非基于比较的排序,可以突破NlogN的时间下限。当然,非比较的排序,也是需要有一些限定条件的。 ### 桶排序 bucket sort 比如给全校学生做个分数排序,最大分100分。我们使用一个100个空间的辅助数据,以key为分数,value为命中的次数。通过O(n)复杂度就可以完成排序任务。这种排序方式就是桶排序。 也就是分配一个hash[100]的空间,初始化为0,遍历一遍,出现的数字就hash[k]++,这样再次遍历一次,就可以得到n个数的顺序了。 ================================================ FILE: 6 Sort/insert_sort.c ================================================ #include "stdio.h" #include "stdlib.h" #include "time.h" // 插入排序 void insert_sort(int *a,int length){ int tmp ; int i,j; for (i = 1; i < length; ++i) { for ( tmp=a[i],j=i-1 ; j>=0 && a[j] > tmp ; j--) { a[j+1]=a[j]; } // j+1是插入的位置 a[j+1]=tmp; } } // 选择排序 void select_sort(int *a,int length){ int min_index,tmp; int j; for (int i = 0; i < length; ++i) { for (j = i+1 ,min_index=i; j < length; ++j)// 不能写成for (int j = i+1 ,min_index=i; j < length; ++j) { if (a[min_index]>a[j]) { min_index=j; } } //min_index是最小的元素的index if (min_index!=i) { tmp=a[i]; a[i]=a[min_index]; a[min_index]=tmp; } } } // 冒泡排序 void bubble_sort(int *a, int length){ int tmp ; for (int i = 0; i < length-1; ++i) // 第i轮排序 { for (int j = 0; j < length-i; ++j) { if (a[j] > a[j+1]) { tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; } } } } // 快速排序 // 挖坑填数,2边向中间扫描 int partion(int *a, int start,int end){ int i=start,j=end; int tmp = a[i]; // 这里要做越界检查 while(i=tmp){ j--; } if (i= a[max])// parent > parent >= max(left, right) { break; } else { heap_swop(&a[parent],&a[max]); parent = max; //继续向下, 比对, 交换, 保证所有树 父节点 >= 子节点 max = 2 * parent + 1; } } } // 从第一个非叶子节点a[(length-2)/2],开始做调整, 跟自己的子节点比较,把最大的孩子换上来就是创建最大堆, //反之,把最小的孩子换上来就是创建最小堆 一直到a[0] void heap_build(int *a,int length){ for (int i = (length-2)/2; i >=0 ; --i) { // 三个数里取最大的一个 a[i],a[2i+1],a[2i+2],跟a[i]交换;然后是 a[(i-1)/2],a[i],a[i+1] .. 一直到a[0] heap_public_adjust(a,i,length); } } // 自顶向下调整 void heap_adjust(int *a,int length){ heap_public_adjust(a,0,length); //对0号调整 } void heap_sort(int *a, int length){ // 建立堆 大根堆,递增排序 heap_build(a,length); for (int i = length-1; i >0; --i) { //交换 heap_swop(&a[0],&a[i]); //调整 heap_adjust(a,i); } } // 归并排序 // 合并2个有序数组,分配一个临时空间,装a,b的结果,最后,将合并结果拷贝到数组A,是否临时空间 void merge_array(int *a,int size_a,int *b, int size_b){ int *tmp = malloc( (size_a+size_b)*sizeof(int) ); int i,j,k; i=j=k=0; while(ib[j])?b[j++]:a[i++]; } while(i1) { merge_sort(a,length/2); merge_sort(a+length/2,length-length/2); merge_array(a,length/2,a+length/2,length-length/2); } } /////////////////////////////////////////////// #define Max_Number 5000 int main(){ //int a[] = {4,87,2,32,5,2,9,49,49,23,45,2,41}; //准备5000个数 int a[Max_Number]; for (int i = 0; i < Max_Number; ++i) { a[i]=rand()%Max_Number; } clock_t start,finish; start = clock(); //merge_sort(a,sizeof(a)/sizeof(int)); // 0.002s,可以看到,归并排序还是很快的 heap_sort(a,sizeof(a)/sizeof(int)); // //quicksort(a,0,sizeof(a)/sizeof(int)-1); // 0.01s //insert_sort(a,sizeof(a)/sizeof(int)); // 3.85s //select_sort(a,sizeof(a)/sizeof(int)); // 5.3s //bubble_sort(a,sizeof(a)/sizeof(int)); // 12.5s finish = clock(); printf("after sort:\n"); for (int i = 0; i < sizeof(a)/sizeof(int); ++i) { printf(" %d ",a[i]); } printf("time eclipse: %.6f sec\n", (double)(finish-start)/CLOCKS_PER_SEC); // CLOCKS_PER_SEC 1000 clock()是毫秒 return 0; } ================================================ FILE: 6 Sort/外排序.md ================================================ # 外排序 ================================================ FILE: 7 Search/README.md ================================================ # 查找算法 * 顺序查找 * 二分查找 * 分块查找 * 动态查找 * 哈希表 ## 顺序查找 顺序表查找。复杂度O(n) ## 二分查找 有序表中查找我们可以使用二分查找。 ``` /* eg: [1,3,5,6,7,9] k=6 @return 返回元素的索引下表,找不到就返回-1 */ int binary_search(int *a,int length,int k){ int low = 0; int high = length-1; int mid; while(low k) high = mid-1; } return -1; } ``` ``` 注意细节 mid+1/mid-1 , 否则的话,有可能死循环 while(low <= high) 而不是 while(low target) { right = mid; // 注意 , 这里没有 -1 } } return left; } ``` ## 分块查找 块内无序,块之间有序;可以先二分查找定位到块,然后再到块中顺序查找。 ## 动态查找 这里之所以叫 动态查找表,是因为表结构是查找的过程中动态生成的。查找结构通常是二叉排序树,AVL树,B- ,B+等。这部分的内容可以去看『二叉树』章节 ## 哈希表 哈希表以复杂度O(1)的成绩位列所有查找算法之首,大量查找的数据结构中都可以看到哈希表的应用。 ================================================ FILE: 8 Algorithms Analysis/README.md ================================================ # 算法分析思路 详细介绍每一种算法设计的思路,并为每种方法给出一个经典案例的详细解读,总结对应设计思路,最后给出其它案例,以供参考。 * [递归](递归.md) * [分治算法](分治算法.md) * [动态规划](动态规划.md) * [回溯法](回溯法.md) * [迭代法](迭代法.md) * [穷举搜索法](穷举搜索法.md) * [贪心算法](贪心算法.md) ### 总结 `贪心法`、`分治法`、`动态规划` 都是将问题归纳为根小的、相似的子问题,通过求解子问题产生全局最优解。 ## 参考 《算法设计与分析基础》 Anany Levitin ================================================ FILE: 8 Algorithms Analysis/分治算法.md ================================================ # 分治算法 将一个难以直接解决的大问题,分割成一些规模较小的相同问题,各个击破,分而治之。 分治算法常用[递归](./递归.md) 实现 1)问题缩小的小规模可以很容易解决 2)问题可以分解为规模较小相同问题 3)子问题的解可以合并为该问题的解 4)各个子问题相互独立,(如果这条不满足,转为`动态规划`求解) 分治法的步骤: 1. 分解 2. 解决 3. 合并 #### 大整数乘法 如 `26542123532213598*345987342245553677884` #### 其它案例 * 快速排序 * [归并排序](../6%20Sort/README.md) * 最大子数组和 ================================================ FILE: 8 Algorithms Analysis/动态规划.md ================================================ # 动态规划DP 动态规划(英语:Dynamic programming,简称 DP),通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。复杂问题不能分解成几个子问题,而分解成一系列子问题 ; DP通常基于一个递推公式及一个(或多个)初始状态,当前子问题解由上一次子问题解推出。 动态规划算法的关键在于解决冗余,以空间换时间的技术,需要存储过程中的各种状态。可以看着是`分治算法`+`解决冗余` 动态规划算法也可以说是 `记住求过的解来节省时间` ; 比如 Fibonacci数列 中,先直接从最小,最简单的 f(1) , f(2) 开始,自低向上一直到 f(20) , 这就是动态规划的思路 【初始状态】→【决策1】→【决策2】→…→【决策n】→【结束状态】 ### DP 应用场景 如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。 使用动态规划算法的问题的特征是`子问题的重叠性`,`最优子结构` ,否则动态规划算法不具备优势。 动态规划的核心思想就是穷举求最值; 动态规划问题的一般形式就是`求最值`,动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说: * Fibonacci数列 [代码参考这里 递归](./递归.md) * 最大子数组和 * 凑零钱问题 * 股票问题 [代码参考这里](https://github.com/nonstriater/deep-in-java/blob/master/src/main/java/com/nonstriater/deepinjava/algo/list/stock/BestChance.java) * 打家劫舍问题 : num[i] 代表第i个房子中的现金数目,从房子中取钱的最大数目,约束是相邻房子的钱不能同时取出 * 接雨水问题 :num[i]表示柱子高度,计算下雨之后能接多少雨水 * 青蛙跳阶问题: 一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法。 * 最小编辑距离 * 最长递增子序列 (LIS Longest Increasing Subsequence),如`【5,6,7,3,2,8】` 最长子序列 `【5,6,7,8】`, 输出4 * 最长公共子序列 (LIS Longest public Subsequence) * 最长回文子序列 (LIS Longest public Subsequence) * 0-1 背包问题 [更多动态规划案例代码实现参考deep-in-java])(https://github.com/nonstriater/deep-in-java/tree/master/src/main/java/com/nonstriater/deepinjava/algo/framework/dynamic) 青蛙跳阶问题 ``` 想跳到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。 同理,要想跳到第9级台阶,要么是先跳到第8级,然后再跳1级台阶上去;要么是先跳到第7级,然后一次迈2级台阶上去。 要想跳到第8级台阶,要么是先跳到第7级,然后再跳1级台阶上去;要么是先跳到第6级,然后一次迈2级台阶上去 即通用公式为: f(n) = f(n-1) + f(n-2) 那f(2) 或者 f(1) 等于多少呢? 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2; 当只有1级台阶时,只有一种跳法,即f(1)= 1; ``` ### DP VS 分治法 与[分治法](分治算法.md)不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。 ### DP VS 回溯法 DP 和 回溯法 都会用到递归 动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划;而有些问题没有重叠子问题,也就是[回溯算法](回溯法.md)问题了,复杂度非常高是不可避免的 ## DP 解题模板 基本步骤 * 划分问题 * 状态定义, 穷举「状态」, bad case * 状态转移方程, 这一步最为困难 ; 暴力解法就是状态转移方程 * 状态压缩 ``` # 初始化 base case dp[0][0][...] = base # 进行状态转移 for 状态1 in 状态1的所有取值: for 状态2 in 状态2的所有取值: for ... dp[状态1][状态2][...] = 求最值(选择1,选择2...) ``` ## 最大子数组和 示例: 输入:nums = [-2,1,-3,4,-1,2,1,-5,4],连续子数组 [4,-1,2,1] 的和最大, 输出:6 dp[i] 表示 nums[i] 为结尾的「最大子数组和」; dp[n-1] 就是 nums 的「最大子数组和」 状态转移 : `dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);` 状态压缩:注意到 dp[i] 仅仅和 dp[i-1] 的状态有关 ```Java public static int largestSubSequenceSum2(int[] nums){ int n = nums.length; if (n == 0) return 0; // base case int dp_0 = nums[0]; int dp_1 = 0; int res = dp_0; for (int i = 1; i < n; i++) { // dp[i] = max(nums[i], nums[i] + dp[i-1]) dp_1 = Math.max(nums[i], nums[i] + dp_0); dp_0 = dp_1; // 顺便计算最大的结果, 保存到 res res = Math.max(res, dp_1); } return res; } ``` ## 凑零钱问题 凑零钱问题[视频解读参考这里](https://www.ixigua.com/6881883015832666635?wid_try=1) [实现代码这里](https://github.com/nonstriater/deep-in-java/tree/master/src/main/java/com/nonstriater/deepinjava/algo/framework/dynamic) 如果使用贪心策略,并不能得到最优解。 比如:给定一个面值list : 1,2,4,5,7,10; 给定一个 target : 14, 求 凑齐 target=14 , 最少的零钱数量 思路: 如想求 amount = 14 时的最少硬币数, 如果你知道凑出 amount = 13 的最少硬币数(子问题), 再加 1 个 1元面值 即可 如果你知道凑出 amount = 12 的最少硬币数(子问题), 再加 1 个 2元面值 即可 如果你知道凑出 amount = 10 的最少硬币数(子问题), 再加 1 个 4元面值 即可 如果你知道凑出 amount = 9 的最少硬币数(子问题), 再加 1 个 5元面值 即可 ```Java for (int coin : coins) { // 计算子问题的结果 int subProblem = dp(coins, amount - coin); // 子问题无解则跳过 if (subProblem == -1) continue; // 在子问题中选择最优解,然后加一 res = Math.min(res, subProblem + 1); } ``` 通过备忘录消除子问题(不用递归了), dp 数组的定义:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出 如想求 amount = 14 时的最少硬币数, dp[14] dp[0] = 0 dp[1] = 1 dp[2] = 1个2元的,dp[1] + 1个1元 ... dp[9] = dp[8] + 1个1元, dp[7] + 1个2元, dp[5] + 1个4元, dp[4] + 1个5元,dp[2] + 1个7元 1 + dp[i-coin] 从这些可选项里选择最小的 ```Java //对于 dp[i], 遍历可选项, 选择最小的 for(int coin : coins) { if (i < coin) { continue; } //dp[i] 保留最小的 dp[i] = Math.min(dp[i],dp[i-coin] + 1 ) } ``` 完整代码如下: ```Java //递归解法,处理重叠子问题, 使用 dp[amount+1] 备忘录 int coinChange2(int[] coins, int amount) { int[] dp = new int[amount + 1]; // 数组大小为 amount + 1,初始值也为 amount + 1 // 为啥 dp 数组初始化为 amount + 1 呢? // 因为凑成 amount 金额的硬币数最多只可能等于 amount(全用 1 元面值的硬币),所以初始化为 amount + 1 就相当于初始化为正无穷 Arrays.fill(dp, amount + 1); // base case dp[0] = 0; // 外层 for 循环在遍历所有状态的所有取值 for (int i = 0; i < dp.length; i++) { // 内层 for 循环在求所有选择的最小值 for (int coin : coins) { // 子问题无解,跳过 if (i - coin < 0) { continue; } dp[i] = Math.min(dp[i], 1 + dp[i - coin]); } } return (dp[amount] == amount + 1) ? -1 : dp[amount]; } ``` ## 股票问题 num[i] 表示第 i 天的股票价格, 设计一个算法(交易策略) ,计算你能获得的最大收益,你最多可以完成 k 笔交易; 比如: [3,2,6,5,1,3] k=1 , 第2天买入 2块钱, 第3天卖出 6块,利润 6-2 = 4块 是最大利润 k= 2 (可以交易2次) , [2,6] + [1,3] 是最大利润 动态规划思路如下: * 状态定义: * 状态转移: * 状态压缩: ``` ``` ## 接雨水问题 num[i]表示柱子高度,计算下雨之后最多能接多少雨水 位置i能装多少? 位置 i 能达到的水柱高度和其左边的最高柱子、右边的最高柱子有关,我们分别称这两个柱子高度为 l_max 和 r_max;位置 i 最大的水柱高度就是 `min(l_max, r_max)` 思路: * 暴力解法 * 备忘录解法 * 双指针解法 ``` ``` ================================================ FILE: 8 Algorithms Analysis/回溯法.md ================================================ # 回溯法 也叫 `试探法`。 是一种选优搜索法,按照选优条件搜索,当搜索到某一步,发现原先选择并不优或达不到目标,就退回重新选择。 回溯算法其实就是我们常说的 DFS 算法,本质上就是一种暴力穷举算法。 一般步骤 1. 针对问题,定义解空间( 这时候解空间是一个集合,且包含我们要找的最优解) 2. 组织解空间,确定易于搜索的解空间结构,通常组织成`树结构` 或 `图结构` 3. 深度优先搜索解空间,搜索过程中用剪枝函数避免无效搜索 回溯法求解问题时,一般是一边建树,一边遍历该树;且采用非递归方法。 #### 案例 * 迷宫问题 * 全排列 ## 代码框架 ```python result = [] def backtrack(路径, 选择列表): if 满足结束条件: result.add(路径) return for 选择 in 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择 ``` ## 全排列问题 > n 个不重复的数,全排列共有 n! 个 ```Java // 路径:记录在 track 中 // 选择列表:nums 中不存在于 track 的那些元素 // 结束条件:nums 中的元素全都在 track 中出现 void backtrack(int[] nums, LinkedList track) { // 触发结束条件 if (track.size() == nums.length) { res.add(new LinkedList(track)); return; } for (int i = 0; i < nums.length; i++) { // 排除不合法的选择 if (track.contains(nums[i])) continue; // 做选择 track.add(nums[i]); // 进入下一层决策树 backtrack(nums, track); // 取消选择 track.removeLast(); } } ``` ## 八皇后问题 8x8的国际象棋棋盘上放置8个皇后,使得任何一个皇后都无法直接吃掉其他的皇后。任意2个皇后都不能处于同一个 横线,纵线,斜线上。 分析 1. 任意2个皇后不能同一行,也就是每个皇后占据一行,通用的,每个皇后也要占据一列 2. 一个斜线上也只有一个皇后 ================================================ FILE: 8 Algorithms Analysis/穷举搜索法.md ================================================ # 穷举搜索法 或者叫蛮力法。对可能的解的众多候选按照某种顺序逐一枚举和检验。典型的问题如选择排序和冒泡排序。 #### 背包问题 给定n个重量为 w1,w2,...,wn,定价为 v1,v2,...,vn 的物品,和一个沉重为W的背包,求这些物品中一个最有价值的子集,且能装入包中。 #### 其它案例 选择排序 冒泡排序 ================================================ FILE: 8 Algorithms Analysis/贪心算法.md ================================================ # 贪心算法 不追求最优解,只找到满意解。 #### 其它案例 * 跳跃游戏 * 射击气球 * 装箱问题 ## 赫夫曼编码 ================================================ FILE: 8 Algorithms Analysis/迭代法.md ================================================ # 迭代法 是一种不断用旧值递推新值的过程,分精确迭代和近视迭代。是用来求方程和方程组近似根的方法。 迭代变量 迭代关系, 迭代关系选择不合理,会导致迭代失败 迭代过程控制,也就是迭代什么时候结束,不能无休止进行下去 ================================================ FILE: 8 Algorithms Analysis/递归.md ================================================ # 递归 递归是一种设计和描述算法的有力工具。 也是回溯法和动态规划的基础。 递归算法执行过程分 `递推` 和 `回归` 两个阶段 在 `递推` 阶段,将大的问题分解成小的问题 在 `回归` 阶段,获得最简单问题的解后,逐级返回,依次得到稍微复杂情况的解,知道获得最终的结果 1) 确定递归公式 , 比如 斐波那契数列 问题中的 `fib(n)=fib(n-1)+fib(n-2)` 2) 确定边界条件 bad case > 自顶向下的递归,自底向上是迭代 递归运行效率较低,因为有函数调用的开销,递归多次也可能造成栈溢出。 ### 递归公式 [快排](../6%20Sort/README.md) ```Java void quicksort(int *a, int left, int right){ if (left ``` ## 判断字符串是否是回文 > 回文,如 abcdcba 分析: 2个指针,一头一尾,逐个比较,都相同就是回文 ``` /* * eg acdeedca * @ret 0 success , -1 fail */ int is_huiwen(const char *source){ if (source == NULL || source == '\0') return -1; char *head = source; char *tail = source; while(*tail != '\0'){ tail++; } tail--; while(head < tail){ if (*head != *tail){ return -1; } head++; tail--; } return 0; } ``` ## 统计文章里单词出现的次数 设计相应的数据结构和算法,尽量高效的统计一片英文文章(总单词数目)里出现的所有英文单词,按照在文章中首次出现的顺序打印输出该单词和它的出现次数。 ``` void statistics(char *string) void statistics(FILE *fd) ``` 延伸: 如果是海量数据里面统计top-k次数的单词呢? ## 实现字符串转整型的函数 也就是实现函数atoi的功能,这道题目考查的就是对各种情况下异常处理。比如: 以`213`分析转换成证书的过程。`3+1x10+2x100` ,思路是:每扫描到一个字符,把 `之前得到的数字*10`,再加上当前的数字 * 0开头,"0213" * 正/负数,"-432" ,"--422","++23" * 浮点数,"43.2344" * 非法,"2123des" * 存在空格," -32"," +432"," 234","23 432","353 "," + 321" * NULL/空串,这时候返回值0 * 溢出,"32111111112222222222222222222222222222222" , 与 `INT_MAX `比较 * 如何区分正常的'0'和异常情况下返回的结果"0"? 可以通过一个全局变量 g_status 来标示,值为 kValid/kInvalid。 ``` int atoi(const char *str){ } ``` 详细过程也可以[参考这里](http://blog.csdn.net/v_july_v/article/details/9024123) ## 匹配兄弟字符串 什么是兄弟字符串? 如果两个字符串的字符一样,但是顺序不一样,被认为是兄弟字符串,问如何在迅速匹配兄弟字符串(如,bad和adb就是兄弟字符串)。 思路:判断各自素数乘积是否相等。更多方法请参见:http://blog.csdn.net/v_JULY_v/article/details/6347454。 ``` int isBrother(const char *first,const char *secd) ``` 思路一: 循环匹配 指数级复杂度 思路二: 利用质数,平方和比较,但这样必须是2个串的长度要一样,需要的空间比较大,最多256个字节。 示例代码: ``` int isBrother(const char *first,const char *secd){ } ``` ## 字符串的排列 题目:输入一个字符串,打印出该字符串中字符的所有排列。例如输入字符串abc,则输出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab和cba。输入字符串 abcca,则输出由 a,b,c排列出来的所有字符串,字符出现个数不变 分析:这是一道很好的考查对递归理解的编程题 简单的回溯就可以实现了。当然排列的产生也有很多种算法,去看看组合数学,还有逆序生成排列和一些不需要递归生成排列的方法。 >印象中Knuth的第一卷里面深入讲了排列的生成。这些算法的理解需要一定的数学功底,也需要一定的灵感,有兴趣最好看看。 ## n个字符串联接 有n个长为m+1的字符串,如果某个字符串的最后m个字符与某个字符串的前m个字符匹配,则两个字符串可以联接,问这n个字符串最多可以连成一个多长的字符串,如果出现循环,则返回错误。 ## 字符串的集合合并 给定一个字符串的集合,格式如:{aaa bbb ccc}, {bbb ddd},{eee fff},{ggg},{ddd hhh}要求将其中交集不为空的集合合并,要求合并完成后的集合之间无交集,例如上例应输出{aaa bbb ccc ddd hhh},{eee fff}, {ggg}。 ================================================ FILE: 9 Algorithms Job Interview/1.1 字符串-查找.md ================================================ #字符串-查找 * 子串查找 ## 找到第一个只出现一次的字符 在一个字符串中找到第一个只出现一次的字符。如输入ahbaccdeff,则输出h。 ``` char char_first_appear_once(const char *source) ``` 思路一: 蛮力统计, O(n^2)复杂度 思路二: 使用hash表,2次扫描 * 第一次建立hash表 key为字符,value为出现次数; * 第二次扫描找到第一个value为1的key,时间复杂度O(n) hash表长度 256,字符直接作为key值。需要注意的是 char 的范围是 -128~127,unsigned char 才是0~255 示例代码: ``` char char_first_appear_once(const unsigned char *source){ int hash[256]={0}; char *tmp = source; if (tmp == NULL) return '\0'; //第一次建立hash表 key为字符,value为出现次数 while(*tmp != '\0'){ hash[*tmp]++; tmp++; } //第二次扫描找到第一个value为1的key tmp = source; while(*tmp != '\0'){ if (hash[*tmp] == 1) return *tmp; tmp++; } return '\0'; } ``` 题目扩展:这里的字符换成整数,整数数量几十TB,海量数据处理,显然hash方法不可能,没有那么大得内容 ### 字符串中找出连续最长的数字子串 写一个函数,功能: 在字符串中找出连续最长的数字串,并把这个串的长度返回,并把这个最长数字串赋值给其中一个函数参数outputstr所指内存。 例如:"abcd12345ed125ss123456789"的首地址传给intputstr后,函数将返回9,outputstr所指的值为123456789 它的原形是: ``` int longest_continuious_number(const char *input,char *output) ``` 应该有3个指针,第一个指针指向一个当前最长数字串的第一个数字,第二个指针指向第二个数字串的第一个数字,第三个指针是遍历指针,且统计第二个数字串的长度;当统计出来的长度大于第一个数字串的长度,第一个指针指向第二个指针指向的数字,相反,第二个指针和第三个指针继续向后查找。 1. 当end首次碰到数字时,且tmp=0,说明是首次出现数字,第二个指针移到该数字,继续遍历 2. 如果数字后面还是数字,tmp!=0 就是第二个数字串,因此 tmp += 1; 3. 当end从数字到普通字符时,如果tmp > max ,就要修改max和第一个指针start ,并把tmp归为0 ``` int longest_continuious_number(const char *input,char *output){ int max = 0; char *start= input; char *mid = input; char *end = input; int tmp = 0; if (input == NULL || output == NULL) return 0; while (*end != '\0'){ if (*end < '0' || *end < '9'){//字母 if(tmp > max){ max = tmp; start = mid; } tmp = 0; }else{//数字 if (tmp == 0){//发现数字 mid = end; } tmp++; } end++; } //修改已数字结尾的bug if(tmp > max){ max = tmp; start = mid; } //copy int i=0; while(i window; int left = 0, right = 0; int res = 0; // 记录结果 while (right < s.size()) { char c = s[right]; right++; // 进行窗口内数据的一系列更新 window[c]++; // 判断左侧窗口是否要收缩 while (window[c] > 1) { char d = s[left]; left++; // 进行窗口内数据的一系列更新 window[d]--; } // 在这里更新答案 res = max(res, right - left); } return res; } ``` ## 对称子字符串的最大长度 (最长回文子串) 题目:输入一个字符串,输出该字符串中对称的子字符串的最大长度。比如输入字符串“google”,由于该字符串里最长的对称子字符串是“goog”,因此输出4。 分析:可能很多人都写过判断一个字符串是不是对称的函数,这个题目可以看成是该函数的加强版。 ``` int max_symmetrical_char_length(const char *scr); ``` * 思路一:蛮力法,3重循环(类似 求子数组的最大和 fmax(i,j)问题), fmax(i,j)区间i,j是最长的对称字符 * 思路二:遍历所有子串,然后判读是否对称 O(n^2) * 思路三:有个O(n)复杂度的算法 http://www.cnblogs.com/McQueen1987/p/3559497.html 分析过程如下: ``` int max_symmetrical_char_length(const char *scr){ } ``` ```Java public String longestPalindrome(String s) { String res = ""; for (int i = 0; i < s.length(); i++) { // 以 s[i] 为中心的最长回文子串 String s1 = palindrome(s, i, i); // 以 s[i] 和 s[i+1] 为中心的最长回文子串 String s2 = palindrome(s, i, i + 1); // res = longest(res, s1, s2) res = res.length() > s1.length() ? res : s1; res = res.length() > s2.length() ? res : s2; } return res; } //实现一个函数来寻找最长回文串 String palindrome(String s, int l, int r) { // 防止索引越界 while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { // 向两边展开 l--; r++; } // 返回以 s[l] 和 s[r] 为中心的最长回文串 return s.substring(l + 1, r); } ``` ## 最小覆盖子串 (难度 hard) `滑动窗口` 给定 字符串 s, t ; 在字符串 s 里找出 包含 t 所有字母的最小子串 eg: s = "ADOBECODEBANC" t = "ABC" 输出: "BANC" ## 最长公共子串问题 请编写一个函数,输入两个字符串,求它们的最长公共子串,并打印出最长公共子串。 例如:输入两个字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它们的最长公共子串,则输出它们的长度4,并打印任意一个子串。 ``` int longest_common_subsequence(const char *s1,const char *s2, char *common) ``` 分析:求最长公共子串(Longest Common Subsequence,LCS)是一道非常经典的动态规划题,因此一些重视算法的公司像MicroStrategy都把它当作面试题。如"abccade","dgcadde"的最大子串为"cad" 实例代码: ``` int longest_common_subsequence(const char *s1,const char *s2, char *common){ } ``` ## 求最大连续递增数字子串 如“ads3sl456789DF3456ld345AA”中的“456789”就是所求。这道题在上一道题目的基础上增加了数字要递增的条件。思路跟上面差不多,碰到不递增的数字就相当于第二个数字串了。 ## 请编写能直接实现strstr()函数功能的代码。 > strstr(str1,str2) 判断str2是否是str1的子串。 ``` /* @ret 有就返回第一次出现子串的地址,否则返回NULL */ char *strstr(const char *source, const char *target){ } ``` ## 子串匹配的个数 已知一个字符串,比如asderwsde,寻找其中的一个子字符串比如sde的个数,如果没有返回0,有的话返回子字符串的个数。 ``` char *substr_count(const char *src, const char *substr, int *count) ``` ================================================ FILE: 9 Algorithms Job Interview/1.2 字符串-删除.md ================================================ # 字符串-删除 ### 删除串中指定的字符 删除指定的字符以后,后面的字符都要向前移动一位。这种复杂度是O(N^2);那么有没有O(N)的方法呢? 比如 "abcdeccba" 删除字符 "c"。使用2个指针,一前一后,比较前面的指针和删除字符: 1. 不相等,两个指针一起跑,且前面的指针值拷贝到后面指针指向的空间 2. 相等时,快指针向前一步 ``` char *delete_occurence_character(char *src , char target){ char *front = src; char *rear = src; while(*front != '\0'){ if (*front != target){ *rear = *front; rear++; } front++; } *rear = '\0'; return src; } ``` ### 在字符串中删除特定的字符 题目:输入两个字符串,从第一字符串中删除第二个字符串中所有的字符。例如,输入”They are students.”和”aeiou”,则删除之后的第一个字符串变成”Thy r stdnts.”。 这是上个题目的升级版本。 ``` char *delete_occurence_characterset(char *source,const char *del); ``` 1. 蛮力法。 遍历字符串,每个字符去删除字符串集合中查找,有就删除 2. 使用上面的方式,一次遍历 ### 删除字符串中的数字并压缩字符串 如字符串”abc123de4fg56”处理后变为”abcdefg”。注意空间和效率。(下面的算法只需要一次遍历,不需要开辟新空间,时间复杂度为O(N)) 这道题跟上一道题也是一个意思。 示例代码: ``` char *trim_number(char *source){ char *start = source; char *end = source; if (source == NULL) return NULL; while(*end != '\0'){ if (*end < '0' || *end > '9' ){ *start = *end; start++; } end++; } *start = '\0'; return source; } ``` ================================================ FILE: 9 Algorithms Job Interview/1.3 字符串-修改.md ================================================ # 字符串-修改 ## 翻转句子中单词的顺序 题目:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。句子中单词以空格符隔开。为简单起见,标点符号和普通字母一样处理。 例如输入“I am a student.”,则输出“student. a am I”。 ``` char *revert_by_word(char *source); ``` 思路: * 原地逆序,字符串2边的字符逐个交换 , 再按单词逆序; * 也可以先按单词逆序,再对整个句子逆序; 针对不允许临时空间的情况,也就是字符交换不用临时空间,可以使用的方法有: 1. 异或操作 2. 也就是2个整数相互交换一个道理 ``` char a = 'a', b = 'b'; a = a + b; b = a - b; a = a - b; ``` 最终示例代码: ``` //反转 void _reverse(char *start,char *end){ if ((start == NULL) || (end == NULL)) return; while(start < end){ char tmp = *start; *start = *end; *end = tmp; start++, end--; } } char *revert_by_word(char *source){ char *end = source; char *start = source; if (source == NULL) return NULL; //end指针挪动到尾部 while (*end != '\0') end++; end--; //先全部反转 _reverse(start,end); //按单词反转 start=end=source; while(*start != '\0'){ if (*start == ' '){ start++; end++; }else if(*end == ' ' || *end == '\0'){ _reverse(start,end-1); start = end; }else{ end++; } } return source; } ``` 类似的题目还有: 不开辟用于交换数据的临时空间,如何完成字符串的逆序 用C语言实现一个revert函数,它的功能是将输入的字符串在原串上倒序后返回。 ## 替换空格 实现一个函数,把每个空格替换成 "%20",如输入“we are happy”,则输出“we%20are%20happy” ``` char *replce_blank(char *source) ``` 主要问题是一个字符替换成3个字符,替换后的字符串比原串长。 如果想要在原串上直接修改,就不能顺序替换。且原串的空间应该足够大,能容纳替换变长以后的字符串。如果空间不够,就要新建一块空间来保存替换的结果了。这里假设空间足够 1. 第一遍扫描,统计空格个数 n, 替换后的字符串长度 = 原长度+2*n 2. 从后向前扫描字符串,挪动每个字符的位置。注意碰到空格的地方 ``` char *replace_blank(char *source){ int count = 0; char *tail = source; if (source == NULL) return NULL; while(*tail != '\0'){ if (*tail == ' ') count++; tail++; } while(count){ if(*tail != ' '){ *(tail+2*count) = *tail; }else{ *(tail+2*count) = '0'; *(tail+2*count-1) = '2'; *(tail+2*count-2) = '%'; count--; } tail--; } return source; } ``` ## 左旋转字符串 >字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。 如把`字符串abcdef`左旋转2位得到`字符串cdefab` 。请实现字符串左旋转的函数。要求时间对长度为n的字符串操作的复杂度为O(n),辅助内存为O(1)。 ``` char *left_rotate(char *str,int offset){ } ``` 思路: 我们可以abcdef分成两部分,ab和cdef,内部逆序以后,整体再次逆序,就可以得到想要的结果了。 也就是跟上面的问题是同样的问题。 ## 字符串原地压缩 题目描述:“abeeeeegaaaffa" 压缩为 "abe5ag3f2a",请编程实现。 这道题需要注意: 1. 单个字符不压缩 2. 注意考虑压缩后某个字符个数是多位数(超过10个) 3. 原地压缩最麻烦的地方就是数据移动 这是使用2个指针,一前一后,如果不相等,都往前移动一位;如果相等,后一位变为数字2,且移动后面的指针一位,任然相等则数字加1,不相等 ``` char *compress(const char *src,char *dest){ } ``` 上面的压缩算法可以看到,压缩算法的`效率`验证依赖`给定字符串的特性`,如果'aaaaaaaa....aaa' 这样特征的字符串,使用上面的压缩算法,压缩率接近100%,相反,可能会的0%的压缩率。 ## 编写strcpy 函数 已知strcpy 函数的原型是: ``` char *strcpy(char *strDest, const char *strSrc); ``` 其中strDest 是目的字符串,strSrc 是源字符串。不调用C++/C 的字符串库函数 ================================================ FILE: 9 Algorithms Job Interview/1.4 字符串-排序.md ================================================ # 字符串-排序 ## 小写字母排在大写字母的前面 有一个由大小写组成的字符串,现在需要对他进行修改,将其中的所有小写字母排在大写字母的前面(大写或小写字母之间不要求保持原来次序),如有可能尽量选择时间和空间效率高的算法 c语言函数原型: ``` void proc(char *str) ``` 分析: 比如:HaJKPnobAACPc,要小写字母前面且不要求保存顺序,可以是:anobcHJKPAACP 1. 小写字母 a~z 的 ASCII码值是 97~122,A~Z的 ASCII码值是 65~90;0~9 的ASCII码是 48~57 2. 两边向中间扫描,左边大写右边小写就交换;如果都小写,头指针向前知道找到大写;如果都是大写,尾指针向后找小写; 示例代码 ``` char *proc(char *str){ char *start = str; char *end = str; if (str == NULL) return NULL; while(*end != '\0') end++; end--; while(start < end){ if (*start >= 'A' && *start <= 'Z'){//大写 if (*end >= 'a' && *end <= 'z'){ char tmp = *start; *start = *end; *end = tmp; start++; } end--; }else{//小写 if (*end >= 'A' && *end <= 'Z'){ end--; } start++; } } return str; } ``` ================================================ FILE: 9 Algorithms Job Interview/2 链表.md ================================================ # 链表 链表常常碰到的问题有: * [链表排序](2.1%20链表-排序.md) * [链表删除](2.2%20链表-删除.md) : 如删除链表中的p节点,在p节点前面插入节点q, 要求O(1)复杂度 * [2个链表](2.3%20链表-2条.md) 相交,合并 * 链表反转 * 链表中是否有环 链表结点定义如下: ``` struct ListNode { int m_nKey; ListNode* m_pNext; }; ``` 常用解题思路 * 双指针 * 3指针 * 快慢指针 ## 输出链表中倒数第k个节点 题目:输入一个单向链表,输出该链表中倒数第k个结点。链表的倒数第0个结点为链表的尾指针。 思路一:2遍遍历,先遍历一遍链表算出 n 的值,然后再遍历链表计算第 n - k 个节点 思路二:双指针,一个指针先走k步,然后 两个指针一起同步往前走,前面先走的指针到链表尾部吧,后面的指针刚好是第k个节点 类似的题目还有 `删除第k个节点` `链表的中间结点` : 使用「快慢指针」的技巧 ,慢指针 slow 前进一步,快指针 fast 就前进两步,这样,当 fast 走到链表末尾时,slow 就指向了链表中点。 ## 给定单链表,检测是否有环。 ``` int isLinkCicle(Link *head); ``` 解题思路 1. 暴力 2. 空间换时间,用一个HashMap 3. 快慢指针: 使用两个指针p1,p2从链表头开始遍历,p1每次前进一步,p2每次前进两步。如果p2到达链表尾部,说明无环,否则p1、p2必然会在某个时刻相遇(p1==p2),从而检测到链表中有环。 ## 链表反转 思路一:迭代,三个指针,遍历一遍(0(n)复杂度 思路一:递归实现,较难理解 迭代实现 ``` ``` 递归实现 ``` ListNode reverse(ListNode head) { if (head.next == null) return head; ListNode last = reverse(head.next); head.next.next = head; head.next = null; return last; } ``` ## 从尾到头输出链表 输入一个链表的头结点,从尾到头反过来输出每个结点的值。链表结点定义如下: 思路一: 辅助栈,需要一个栈空间 思路二: 反转链表,然后遍历 思路三: 递归实现,将printf语句放在递归调用后面。果然妙极。。 ## 复杂链表的复制 题目:有一个复杂链表,其结点除了有一个m_pNext指针指向下一个结点外,还有一个m_pSibling指向链表中的任一结点或者NULL。其结点的C++定义如下: ``` struct ComplexNode { int m_nValue; ComplexNode* m_pNext; ComplexNode* m_pSibling; }; ``` 下图是一个含有5个结点的该类型复杂链表。图中实线箭头表示m_pNext指针,虚线箭头表示m_pSibling指针。为简单起见,指向NULL的指针没有画出。 请完成函数`ComplexNode* Clone(ComplexNode* pHead)`,以复制一个复杂链表。 这个题目难点在于: ================================================ FILE: 9 Algorithms Job Interview/2.1 链表-排序.md ================================================ # 链表-排序 ## 链表排序 Given a head pointer pointing to a linked list ,please write a function that sort the list in increasing order. You are not allowed to use temporary array or memory copy (微软面试题) ``` struct { int data; struct S_Node *next; }Node; Node * sort_link_list_increasing_order (Node *pheader): ``` ## 单链表归并排序 啥是归并排序? 类似的题有: 1 给定两个单链表(head1, head2),检测两个链表是否有交点,如果有返回第一个交点。 如果head1==head2,那么显然相交,直接返回head1。 否则,分别从head1,head2开始遍历两个链表获得其长度len1与len2,假设len1>=len2, 那么指针p1由head1开始向后移动len1-len2步,指针p2=head2, 下面p1、p2每次向后前进一步并比较p1p2是否相等,如果相等即返回该结点, 否则说明两个链表没有交点。 2 给定单链表(head),如果有环的话请返回从头结点进入环的第一个节点。 运用题一,我们可以检查链表中是否有环。如果有环,那么p1p2重合点p必然在环中。从p点断开环,方法为:p1=p, p2=p->next, p->next=NULL。此时,原单链表可以看作两条单链表,一条从head开始,另一条从p2开始, 于是运用题二的方法,我们找到它们的第一个交点即为所求。 ================================================ FILE: 9 Algorithms Job Interview/2.2 链表-删除.md ================================================ #链表-删除 ## 在O(1)时间内删除链表结点 题目:给定链表的头指针和一个结点指针,在O(1)时间删除该结点。链表结点的定义如下: ``` struct ListNode{ int m_nKey; ListNode* m_pNext; }; //函数的声明如下: void deleteNode(ListNode* pListHead, ListNode* pToBeDeleted); ``` 思路: 保存下一个节点的值tmp,删除下一个节点,当前节点=tmp ## 删除链表中的p节点 只给定单链表中某个结点p(并非最后一个结点,即p->next!=NULL)指针,删除该结点。 办法很简单,首先是放p中数据,然后将p->next的数据copy入p中,接下来删除p->next即可。 类似的还有问题:只给定单链表中某个结点p(非空结点),在p前面插入一个结点。办法类似,首先分配一个结点q,将q插入在p后, 接下来将p中的数据copy入q中,然后再将要插入的数据记录在p中。都可以做到0(1)复杂度 ## 将两链表中data值相同的结点删除 有双向循环链表结点定义为: ``` struct node{ int data; struct node *front,*next; }; ``` 有两个双向循环链表A,B,知道其头指针为:pHeadA,pHeadB,请写一函数将两链表中data值相同的结点删除。 ================================================ FILE: 9 Algorithms Job Interview/2.3 链表-2条.md ================================================ # 链表-2条 链表的结点定义为: ``` struct ListNode { int m_nKey; ListNode* m_pNext; }; ``` ## 找出两个链表的第一个公共结点 题目:两个单向链表,找出它们的第一个公共结点。 分析:第一个公共节点,也就是2个链表中的节点的m_pNext 指向的同一个节点。 2遍遍历方法: 1. 先遍历2个链表,得到各自的长度,和差sub 2. 长链表先遍历sub个节点,然后2个节点一起遍历 3. 第一次同时指向的同一个节点就是这个commonNode 有没有可能一遍遍历就解决问题呢? 让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于「逻辑上」两条链表接在了一起。 空间复杂度为 O(1),时间复杂度为 O(N), 一遍遍历就搞定 ``` ListNode getIntersectionNode(ListNode headA, ListNode headB) { // p1 指向 A 链表头结点,p2 指向 B 链表头结点 ListNode p1 = headA, p2 = headB; while (p1 != p2) { // p1 走一步,如果走到 A 链表末尾,转到 B 链表 if (p1 == null) p1 = headB; else p1 = p1.next; // p2 走一步,如果走到 B 链表末尾,转到 A 链表 if (p2 == null) p2 = headA; else p2 = p2.next; } return p1; } ``` ## 判断俩个链表是否相交 给出俩个单向链表的头指针,比如h1,h2,判断这俩个链表是否相交。为了简化问题,我们假设俩个链表均不带环。 问题扩展: 1.如果链表可能有环列? 2.如果需要求出俩个链表相交的第一个节点列? ## 合并2条有序链表 while 循环每次比较 p1 和 p2 的大小,把较小的节点接到结果链表上 ``` ListNode mergeTwoLists(ListNode l1, ListNode l2) { // 虚拟头结点 ListNode dummy = new ListNode(-1), p = dummy; ListNode p1 = l1, p2 = l2; while (p1 != null && p2 != null) { // 比较 p1 和 p2 两个指针 // 将值较小的的节点接到 p 指针 if (p1.val > p2.val) { p.next = p2; p2 = p2.next; } else { p.next = p1; p1 = p1.next; } // p 指针不断前进 p = p.next; } if (p1 != null) { p.next = p1; } if (p2 != null) { p.next = p2; } return dummy.next; } ``` ## 合并k个有序链表 相比合并两个有序链表,难点在于,如何快速得到 k 个节点中的最小节点,接到结果链表上? 就要用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点: ``` ListNode mergeKLists(ListNode[] lists) { if (lists.length == 0) return null; // 虚拟头结点 ListNode dummy = new ListNode(-1); ListNode p = dummy; // 优先级队列,最小堆 PriorityQueue pq = new PriorityQueue<>( lists.length, (a, b)->(a.val - b.val)); // 将 k 个链表的头结点加入最小堆 for (ListNode head : lists) { if (head != null) pq.add(head); } while (!pq.isEmpty()) { // 获取最小节点,接到结果链表中 ListNode node = pq.poll(); p.next = node; if (node.next != null) { pq.add(node.next); } // p 指针不断前进 p = p.next; } return dummy.next; } ``` ## 输出两个非降序链表的并集 请修改append函数,利用这个函数实现: 两个非降序链表的并集,1->2->3 和 2->3->5 并为 1->2->3->5 。另外只能输出结果,不能修改两个链表的数据。 ================================================ FILE: 9 Algorithms Job Interview/3 堆和栈.md ================================================ # 堆和栈 栈的数据结构 ``` typede int Value; typedef struct Entry{ Entry *next; Value value; }Entry; typedef struct Stack{ Entry *head;/*添加和删除都在这个节点指针上*/ int length; int capacity; }Stack; Stack create(unsigned int size); void push(Stack,Value); Value pop(Stack); ``` 队列的数据结构 ``` typede int Value; typedef struct Entry{ Entry *next; Value value; }Entry; typedef struct Queue{ Entry *front;/*删除都在这个节点指针上*/ Entry *rear;/*添加在这个节点指针上*/ int length; }Stack; void enqueue(Queue,Value); void dequeue(Queue); ``` 这里的题目是有关栈和队列的问题。 ## 循环队列中元素个数 如果用一个循环数组q[0..m-1]表示队列时,该队列只有一个队列头指针front,不设队列尾指针rear,求这个队列中从队列头到队列尾的元素个数(包含队列头、队列尾) 分析: 有rear指针时: ``` if (rear>front) count = rear-front+1 else count = rear-front+m+1 ``` 综合: `count = (rear-front+m+1)%m` 不用rear指针的话: 数据结构中加一个 `int count`来计数。 ## 设计包含min函数的栈 定义栈的数据结构,要求添加一个min函数,能够得到栈的最小元素。 要求函数min、push以及pop的时间复杂度都是O(1)。 主要是min函数实现。咋一看,用一个变量来存储最小值,或最小值的下标不就ok了?这在只push的时候有用,想想如果这个最小值pop出去了呢?我们怎么更新现在的最小值? 这里用一个辅助栈,空间复杂度是O(n). push一个元素a时,该元素a与辅助栈顶f[top]元素比较: a > f[top],在辅助栈再次中push一遍f[top] (可以把这个省去,节省空间,代价是pop的时候要跟最小栈元素比较,如果pop元素跟最小栈栈顶相等,可能最小值要变了) a < f(top),把 a push 到 辅助栈中 这样辅助栈中的元素是 从上到小 递增的数列。且 辅助栈中的栈顶元素 总是 栈中元素集合的最小值。 以上方法需要的空间复杂度是O(n) ## 栈的push、pop序列 题目:输入两个整数序列。其中一个序列表示栈的push顺序,判断另一个序列有没有可能是对应的pop顺序。为了简单起见,我们假设push序列的任意两个整数都是不相等的。 比如输入的push序列是[1、2、3、4、5] ,那么[4、5、3、2、1]就有可能是一个pop序列 因为可以有如下的push和pop序列: 【push 1,push 2,push 3,push 4,pop,push 5,pop,pop,pop,pop】, 这样得到的pop序列就是【4、5、3、2、1】。 但序列【4、3、5、1、2】就不可能是push序列【1、2、3、4、5】的pop序列。 这里只是要判断是不是pop序列,并没有要求所有的pop序列 需要一个辅助栈(需要一个写好的栈结构辅助,如果是C语言,还要包装一个栈结构) 1. 对序列A中的A[0]入栈,比较B[0] 2. 如果不相等,A[1]入栈 3. 直到A[i] = B[0] 4. 栈顶出栈,开始匹配B[1] 5. 如果A[i-1]!=B[1],那就继续入栈A[i+1] 6 .循环往复~ 7. 如果最后,栈为空,就是一个pop序列;如果栈不为空,或者B数组都没有遍历到尾部,就肯定不是pop序列了 ## 颠倒栈 题目:用递归颠倒一个栈。例如输入栈{1, 2, 3, 4, 5},1在栈顶。颠倒之后的栈为{5, 4, 3, 2, 1},5处在栈顶。 思路一:所有元素pop出来,放到一个数组里,然后在从第一个元素开始入栈,空间复杂度需要 O(N) 思路二: 递归方法。把栈看出2部分 :1 和 {2,3,4,5} , 把栈{2,3,4,5}颠倒过来,然后把1放到底部,那整个栈就颠倒过来了。 ``` void reverseStack(){ stack.pop(); reverseStack(); addTopToStackBottom(); } ``` ================================================ FILE: 9 Algorithms Job Interview/4 数值问题.md ================================================ # 数值问题 这部分都是一些数学几何计算方面的问题。主要由: * [加减乘除](4.1%20数值-加减乘除.md) * 指数(乘方) * 随机数 * 进制转换:位运算 * 大数问题 * 公倍数 * 素数 * 丑数 ## ipv4 转 int 比如: 127.0.0.1 , 转为int 为 (01111111 00000000 00000000 00000001) 思路: 1. IPv4 地址由 4 个字节组成(如 192.168.1.1) 2. 每个字节对应整数的 8 位 3. 转换公式:(first << 24) | (second << 16) | (third << 8) | fourth 4. 使用位运算高效组合各部分 ```C unsigned ipv4_to_int(const char* ip){ unsigned char bytes[4]; const char* start = ip; // 解析四个部分 for (int i = 0; i < 4; i++) { // 查找下一个点或字符串结尾 const char* end = strchr(start, '.'); if (!end) end = start + strlen(start); // 将当前部分转为整数 bytes[i] = (unsigned char)strtoul(start, NULL, 10); // 移动到下一部分 start = end + 1; } // 组合四个字节 return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]; } ``` ```Java public class Ipv4Converter { public static long ipv4ToInt(String ip) { String[] parts = ip.split("\\."); if (parts.length != 4) { throw new IllegalArgumentException("Invalid IPv4 address format"); } long result = 0; for (int i = 0; i < 4; i++) { int value = Integer.parseInt(parts[i]); if (value < 0 || value > 255) { throw new IllegalArgumentException("Invalid IP segment: " + value); } result = (result << 8) | value; } return result; } public static void main(String[] args) { String ip = "192.168.1.1"; long result = ipv4ToInt(ip); System.out.println("IPv4: " + ip); System.out.println("Integer: " + result); System.out.println("Hex: 0x" + Long.toHexString(result)); } } ``` 那么,int 转 ipv4 如何解呢? ## 整数的二进制表示中1的个数 题目:输入一个整数,求该整数的二进制表达中有多少个1。例如输入10,由于其二进制表示为1010,有两个1,因此输出2。 分析: 这是一道很基本的考查位运算的面试题。 解法1:一轮循环移位计数 (移位运算比除法运算效率要高,注意要考虑是负数的情况) 解法2:位运算 解法3:num &= num-1 巧妙之处在于,对高位没有影响。不断做 `num &= num-1` 直到num=0。 1010 & 1001 = 1000 1000 * 0111 = 0000 ``` int one_appear_count_by_binary(int num){ int count = 0; while(num !=0 ){ num &= num-1; count++; } return count; } ``` ## 把十进制数(long型)分别以二进制和十六进制形式输出,不能使用printf系列 分析: ``` char *integer_to_hex(long i); //eg: 20 => 14 char *integer_to_bin(long i); //eg: 20 => 10100 ``` 注意,转16进制中,要判断tmp[i]是否是有符号的数 ``` tmp[i] = tmp[i]>=0 ? tmp[i] : tmp[i]+16; ``` ## 请定义一个宏,比较两个数a、b的大小,不能使用大于、小于、if语句 分析: ``` #define min(a,b) ((a)>(b)?(a):(b)) #define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) ``` 这里不能使用比较符号: ``` #define min(a,b) ((a)-(b) & (0x1<<31))?(a):(b) ``` ## 整数的素数和分解问题 > 歌德巴赫猜想说任何一个不小于6的偶数都可以分解为两个奇素数之和。 对此问题扩展,如果一个整数能够表示成两个或多个素数之和,则得到一个素数和分解式。 对于一个给定的整数,输出所有这种素数和分解式。 注意,对于同构的分解只输出一次(比如5只有一个分解2 + 3,而3 + 2是2 + 3的同构分解式 例如,对于整数8,可以作为如下三种分解: ``` (1) 8 = 2 + 2 + 2 + 2 (2) 8 = 2 + 3 + 3 (3) 8 = 3 + 5 ``` ## 输出1到最大的N位数 题目:输入数字n,按顺序输出从1最大的n位10进制数。比如输入3,则输出1、2、3一直到最大的3位数即999。 分析:这是一道很有意思的题目。看起来很简单,其实里面却有不少的玄机。 输入4,输出: 1,2,3,。。9999 输入5,输出: 1,2,3,4,...99999 玄机一: 整数溢出 ## 寻找丑数 > 我们把只包含因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含因子7。习惯上我们把1当做是第一个丑数。 > 分析:这是一道在网络上广为流传的面试题,据说google曾经采用过这道题。 求按从小到大的顺序的第1500个丑数。 这里的因子应该不包含本身,因此这个序列应该是这样: 1,2,3,4,5,6,8,9,10,12,15,16,18,20,28.... 1)所有的偶数都在序列中 2)3的倍数也在序列中 3)5的倍数也在系列中 0. 2,3,5最小公倍数是30 1. [1,30]符合条件有22个 2. [30,60]符合条件也22个 第1500个: `1500/22=68` 余 4,一个周期内的前4个数是2,3,4,5; 最终答案是`68*30+5` ================================================ FILE: 9 Algorithms Job Interview/4.1 数值-加减乘除.md ================================================ # 数值-加减乘除 * 等差数列 * 阶乘 ## 求1+2+…+n 要求不能使用乘除法、for、while、if、else、switch、case等关键字以及条件判断语句(A?B:C)。 思路:这是等差数列求和公式, 1+2+3.......+N =(n+1)n/2 分析: 1. 不能使用循环,那就用递归 2. 递归需要终止递归的条件判断语句,这里也不能用if,想其他办法,可以使用 &&逻辑与 运算符(在n>0条件满足是,才会指向后面的递归语句) ``` int sum(n){ int sum=0; (n>0) && sum=n+factorial(n-1) return sum; } ``` ## 大数阶乘(factorial) > 阶乘 n*(n-1)*(n-2)*...*1 主要考虑算出来的结果肯定会大于int表达的范围,这时候怎么处理? ``` int factorial(n){ int sum=0; (n>0) && sum=n+factorial(n-1) return sum; } ``` 考虑整数溢出情况 ``` char[] factorial(int n){ int sum=0; return sum; } ``` ## 1024! 末尾有多少个0? 分析: 末尾0的个数取决于2和5的个数 能被2整除的数比能被5整除的数要多得多,因此只统计被5整除的数的个数 ## 大整数乘法(或 大整数阶乘) 请使用代码计算`1234567891011121314151617181920*2019181716151413121110987654321` 。 1. 注意结果可能超出长整形的最大范围 2^64-1 2. 采用分治算法,将大整数相乘转换为小整数计算 规律分析:任意位数的整数相乘,最终都可以转化为2位数相乘 ## 实现两个正整数的除法(和取模) 编程实现两个正整数的除法,当然不能用除法操作符。 ``` int div(const int x, const int y) ``` 1. 循环减被除数,减到不能再减,当除数很大,被除数小时,效率很低 2. 位运算 ## 两个数相乘,小数点后位数没有限制,请写一个高精度算法 ================================================ FILE: 9 Algorithms Job Interview/4.2 数值-指数.md ================================================ # 4.2 数值-指数 > 在a^n中,a叫做底数,n叫做指数。a^n读作“a的n次方”或“a的n次幂“ ## 数值的整数次方 题目:实现函数`double Power(double base, int exponent)`,求base的exponent次方。 不需要考虑溢出。 用例: ``` 2^3 = 8 0^3 = 0 2^0 = 1 2^-3 = 1/(2^3) = 0.125 0^-3 ``` 分析:这是一道看起来很简单的问题。可能有不少的人在看到题目后30秒写出如下的代码: ``` double Power(double base, int exponent) { double result = 1.0; for(int i = 1; i <= exponent; ++i) result *= base; return result; } ``` 上面的代码没有考虑: exponent<=0 判断一个浮点数是不是等于0时,不是直接写 base == 0 ,而应该判断它们的差的绝对值是不是小于一个很小的范围 如果指数大于0,我们还可以使用递归实现:`a^n = a^(n/2)*a^(n/2)` (n为偶数), 通过这个思路也可以实现 ``` 2^16 = 2^8 * 2^8 2^8 = 2^4 * 2^4 2^4 = 2^2 * 2^2 2^2 = 2^1 * 2^1 2^1 = 2 ``` 使用递归实现的代码示例: ``` double Power(double base, unsigned int exponent) { if (exponent == 0) return 1; if (exponent == 1) return base; double result = Power(base, exponent >> 1); result *= result; if (exponent & 1) result = result*base; return reslut; } ``` ## Sqrt(x) 给你一个非负整数 x ,计算并返回 x 的 算术平方根 。由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。 示例 1: ``` 输入:x = 4 输出:2 ``` 示例 2: ``` 输入:x = 8 输出:2 解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。 ``` ## 求根号2的值 并且按照我的需要列出指定小数位,比如根号2是1.141 我要列出1位小数就是1.1 2位就是1.14, 1000位就是1.141...... 等。。 分析: 泰勒级数 牛顿迭代法 ## 判断一个自然数是否是某个数的平方 说明:当然不能使用开方运算。 ``` square(25) YES square(35) NO ``` 方法1: 从1开始遍历,显然这种方法很差 方法2: 除数跟余数比较,除数从2开始,每一轮都跟结果比较;相等就是存在这个数,不相等就把除数++;时间复杂度为(根号n)。在一个数比较大时,效率不够好。如1194877489 =(34567)^2,需要从2开始,一直比较到34566,做3万多次除法和比较运算。跟方法1一样。。 方法3:二分查找 O(logn)。比如25: a. 先取(0+25)/2=12.5,12.5*12.5>25,因此这个数应该小于12.5 b.(0+12)/2 = 6, 6*6>25 c. (0+6)/2 = 3 d. (3+6)/2 = 4.5 e. (5+6)/2 = 5.5 ``` int is_powered(int num) ``` ================================================ FILE: 9 Algorithms Job Interview/4.3 数值-随机数.md ================================================ # 4.3 数值-随机数 ## 给定能随机生成整数1到5的函数,写出能随机生成整数1到7的函数。 ## 设计一个随机算法 给你5个球,每个球被抽到的可能性为30、50、20、40、10,设计一个随机算法,该算法的输出结果为本次执行的结果。输出A,B,C,D,E即可。 ## 构造一个随机发生器 已知一随机发生器,产生0的概率是p,产生1的概率是1-p,现在要你构造一个发生器, 使得它构造0和1的概率均为1/2;构造一个发生器,使得它构造1、2、3的概率均为1/3;..., 构造一个发生器,使得它构造1、2、3、...n的概率均为1/n,要求复杂度最低。 ================================================ FILE: 9 Algorithms Job Interview/4.4 数值-最小公倍数.md ================================================ # 4.4 数值-最小公倍数 求两个或N个数的最大公约数和最小公倍数? > 最大公约数: 12、16的公约数有1、2、4,其中最大的一个是4,4是12与16的最大公约数 > 最小公倍数: 45和30的最小公倍数是多少? > 定理: (a,b)x[a,b]=ab ; (a,b) 是 a,b 的 最大公约数, [a,b]是a,b 的 最小公倍数 ### 最小公倍数应用场景 甲、乙、丙三人是朋友,他们每隔不同天数到图书馆去一次。甲3天去一次,乙4天去一次,丙5天去一次。有一天,他们三人恰好在图书馆相会,问至少再过多少天他们三人又在图书馆相会? 一块砖长20厘米,宽12厘米,厚6厘米。要堆成正方体至少需要这样的砖头多少块? 甲每秒跑3米,乙每秒跑4米,丙每秒跑2米,三人沿600米的环形跑道从同一地点同时同方向跑步,经过多少时间三人又同时从出发点出发? ================================================ FILE: 9 Algorithms Job Interview/4.5 数值-素树.md ================================================ # 数值-素树 > 如果一个数如果只能被 1 和它本身整除,那么这个数就是素数 ## 返回区间 [2, n) 中有几个素数 ``` int countPrimes(int n) ``` ================================================ FILE: 9 Algorithms Job Interview/5 数组数列问题.md ================================================ # 数组数列问题 这部分的问题都集中在数据集合上。主要有: * 数组排序 * top-k * 子数组 * 多个数组合并,交集 解决这一类问题时,可以从以下几个方面考虑: * 万能的蛮力穷举 * 散列表空间换事件 * 分治法,然后归并 (归并排序) * 选择合适的数据结构可以显著提高算法效率 (堆排序求top-k) * 对无序的数组先排序,使用二分 * 贪心算法或动态规划 ## Fibonacci数列 题目:定义Fibonacci数列如下: ``` 0 n=0 f(n)= 1 n=1 f(n-1)+f(n-2) n=2 ``` 输入n,用最快的方法求该数列的第n项。 思路一:`递归`,虽然fibonacci数列是`递归`的经典应用,但递归效率很差,会有很多重复的计算,复杂度是成指数递增的,我测试了下计算50的时候已经要300s了。 ``` int fib(int N) { if (N == 1 || N == 2) return 1; return fib(N - 1) + fib(N - 2); } ``` 思路二:从下往上计算,复杂度O(N),一个循环就搞定 ``` public int fib(int n){ if (n == 0) return 0; //缓存 int[] dp = new int[n + 1]; // base case dp[0] = 0; dp[1] = 1; // 状态转移 for (int i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; } ``` 说一个细节优化点,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1): ``` int fib(int n) { if (n < 1) return 0; if (n == 2 || n == 1) return 1; int prev = 1, curr = 1; for (int i = 3; i <= n; i++) { int sum = prev + curr; prev = curr; curr = sum; } return curr; } ``` ## 递减数列左移后的数组中找数 一个数组是由一个递减数列左移若干位形成的,比如{4,3,2,1,6,5} 是由{6,5,4,3,2,1}左移两位形成的,在这种数组中查找某一个数。 1. 右移,二分查找。 找到最小的数,右移到第一个位置的时候,右移完成 O(N) 2. 直接二分查找 ## 给出一个洗牌算法 给出洗牌的一个算法,并将洗好的牌存储在一个整形数组里 分析:扑克牌54张`2~10,J,Q,K,A,小王,大王` 1)产生随机数, 随机数 rand()%54 ,rand()每次运行都一样,要改为srand(time(NULL)) 2) 遍历数组, 随机数k属于区间[i,n],然后a[i] 和 随机数 a[k] 对换 ## 重合区间最长的两个区间段 在一维坐标轴上有n个区间段,求重合区间最长的两个区间段 如【-7,21】,【4,23】,【14,100】,【54,76】 思路一:两两比较,复杂度 N^2 思路二:先排序+分而治之 在一个int数组里查找这样的数,它大于等于左侧所有数,小于等于右侧所有数。 直观想法是用两个数组a、b。a[i]、b[i]分别保存从前到i的最大的数和从后到i的最小的数, 一个解答:这需要两次遍历,然后再遍历一次原数组, 将所有data[i]>=a[i-1]&&data[i]<=b[i]的data[i]找出即可。 给出这个解答后,面试官有要求只能用一个辅助数组,且要求少遍历一次。 一排N(最大1M)个正整数+1递增,乱序排列,第一个不是最小的,把它换成-1, 最小数为a且未知。求第一个被-1替换掉的数原来的值,并分析算法复杂度。 [4,3,5,2,7,6] 题目啥意思? 正整数序列Q中的每个元素都至少能被正整数a和b中的一个整除,现给定a和b,需要计算出Q中的前几项,例如,当a=3,b=5,N=6时,序列为3,5,6,9,10,12 (1)、设计一个函数void generate(int a,int b,int N ,int * Q)计算Q的前几项 (2)、设计测试数据来验证函数程序在各种输入下的正确性。 分析: 这个输出序列是要 递增排列 思路一: 类似对2各数组merge,取min( A[i],B[j]) .复杂度O(N) 不过这里要注意,去掉 a, b的公倍数。如 3,5 都有15可以整除 ## 把数组排成最小的数 题目:输入一个正整数数组,将它们连接起来排成一个数,输出能排出的所有数字中最小的一个。 例如输入数组{32, 321},则输出这两个能排成的最小数字32132。 请给出解决问题的算法,并证明该算法。 ## 旋转数组中的最小元素。 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个排好序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3, 4, 5, 1, 2}为{1, 2, 3, 4, 5}的一个旋转,该数组的最小值为1。 分析:这道题最直观的解法并不难。从头到尾遍历数组一次,就能找出最小的元素, 时间复杂度显然是O(N)。但这个思路没有利用输入数组的特性,我们应该能找到更好的解法。 ## 约瑟夫环问题 n个数字(0,1,…,n-1)形成一个圆圈,从数字0开始, 每次从这个圆圈中删除第m个数字(第一个为当前数字本身,第二个为当前数字的下一个数字)。 当一个数字删除后,从被删除数字的下一个继续删除第m个数字。 求出在这个圆圈中剩下的最后一个数字。 如 `0,1,2,3,4,5` 删除第2个数字 (n=6,m=2) 第一次删除:1 第二次删除: 3 第三次删除:5 第四次删除:2 因此,左后的一个数字就是 4 从数学上分析下规律: ## 求最大重叠区间大小 题目描述:请编写程序,找出下面“输入数据及格式”中所描述的输入数据文件中最大重叠区间的大小。 对一个正整数 n ,如果n在数据文件中某行的两个正整数(假设为A和B)之间,即A<=n<=B或A>=n>=B ,则 n 属于该行; 如果 n 同时属于行i和j ,则i和j有重叠区间;重叠区间的大小是同时属于行i和j的整数个数。 例如,行(10 20)和(12 25)的重叠区间为 [12 20] ,其大小为9,行(20 10)和( 20 30 )的重叠区间大小为 1 。 ## 四对括号可以有多少种匹配排列方式 比如两对括号可以有两种:()()和(()) ## 两两之差绝对值最小的值 有一个整数数组,请求出两两之差绝对值最小的值,记住,只要得出最小值即可,不需要求出是哪两个数。 如 [-1, 3, 5, 9] 绝对值最小的是2(5-3) 最短区间问题 如果是有重复元素,那最小值就是0了 解题思路: 可以将这个问题转化为 ”求最大字段和“ 问题。。。 ## 数值是否连续相邻 一个整数数列,元素取值可能是0~65535中的任意一个数,相同数值不会重复出现。0是例外,可以反复出现。请设计一个算法,当你从该数列中随意选取5个数值,判断这5个数值是否连续相邻。 注意: - 5个数值允许是乱序的。比如: 8 7 5 0 6 - 0可以通配任意数值。比如:8 7 5 0 6 中的0可以通配成9或者4 - 0可以多次出现。 - 复杂度如果是O(n2)则不得分。 这个问题跟”扑克牌顺子“判断问题一样,通过比较0的个数和相邻数字之间间隔总和来判断所有数是否连续。 ////////////// 数列 /////////////// 给出两个集合A和B,其中集合A={name}, 集合B={age、sex、scholarship、address、...}, 要求: 问题1、根据集合A中的name查询出集合B中对应的属性信息; 问题2、根据集合B中的属性信息(单个属性,如age<20等),查询出集合A中对应的name。 ================================================ FILE: 9 Algorithms Job Interview/5.1 数列-排序.md ================================================ # 5.1 数列-排序 ## 使用归并排序对一个 int 类型的数组排序 比如 [1, 6, 2, 2, 2, 3] ``` void sort(int * a, int length) ``` ## 用递归的方法判断整数组a[N]是不是升序排列 递归 isAscend(n-1) && a[N-1]< a[N] ## 求一个数组的最长递减子序列 比如`{9,4,3,2,5,4,3,2}`的最长递减子序列为`{9,5,4,3,2}` `动态规划` ## 扑克牌的顺子 从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。 2-10为数字本身,A为1,J为11,Q为12,K为13,而大小王可以看成任意数字。 对这5个数(大小王看做0) 1)排序,如快排 2)统计0的个数 3)统计相邻元素空缺总数 ``` //5个是不是顺子 isShunZi(int *a,int length) ``` ## 分割数组 一个int数组,里面数据无任何限制,要求求出所有这样的数a[i],其左边的数都小于等于它,右边的数都大于等于它。 能否只用一个额外数组和少量其它空间实现。 `快速排序` ## 调整数组顺序使奇数位于偶数前面 题目:输入一个整数数组,调整数组中数字的顺序,使得所有奇数位于数组的前半部分, 所有偶数位于数组的后半部分。要求时间复杂度为O(n)。 思路:两边向中间扫描,如果第一个指针是偶数,第二个指针是奇数,就交换;如果第一个是偶数,第二个也偶数,第二个指针向前移;反之,第一个指针向后移 ``` void reorder(int *data, int length); ``` ## 奇偶分离 给定一个存放整数的数组,重新排列数组使得数组左边为奇数,右边为偶数。 要求:空间复杂度O(1),时间复杂度为O(n) 如 [4,5,2,7,5] => [5,7,5,4,2], 空间复杂度O(1),得使用 交换排序 插入排序思想 快速排序思想 1-1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次. 每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空间, 能否设计一个算法实现? 分析:难点在于 `不用辅助空间`。 思路一: `sum(数组元素的总和)-sum(1~1000)` 得到的差即为重复元素,N较大时注意总和溢出 思路二: 异或操作(位运算) ## 重新排列使负数排在正数前面 一个未排序整数数组,有正负数,重新排列使负数排在正数前面,并且要求不改变原来的正负数之间相对顺序 比如: input: 1,7,-5,9,-12,15 ans: -5,-12,1,7,9,15 要求时间复杂度O(N),空间O(1)。(此题一直没看到令我满意的答案,一般达不到题目所要求的:时间复杂度O(N),空间O(1),且保证原来正负数之间的相对位置不变)。 updated:设置一个起始点j, 一个翻转点k,一个终止点L 从右侧起 起始点在第一个出现的负数, 翻转点在起始点后第一个出现的正数,终止点在翻转点后出现的第一个负数(或结束) 如果无翻转点, 则不操作 如果有翻转点, 则待终止点出现后, 做翻转, 即ab => ba 这样的操作 翻转后, 负数串一定在左侧, 然后从负数串的右侧开始记录起始点, 继续往下找下一个翻转点 例子中的就是 1, 7, -5, 9, -12, 15 第一次翻转: 1, 7, -5, -12,9, 15 => 1, -12, -5, 7, 9, 15 第二次翻转: -5, -12, 1, 7, 9, 15 N维翻转空间占用为O(1)复杂度是2N;在有一个负数的情况下, 复杂度最大是2N, ;在有i个负数的情况下, 复杂度最大是2N+2i, 但是不会超过2N+N实际的复杂度在O(3N)以内 但从最终时间复杂度分析,此方法是否真能达到O(N)的时间复杂度,还待后续考证。感谢John_Lv,MikovChain。2012.02.25。 1, 7, -5, -6, 9, -12, 15(后续:此种情况未能处理) 1 7 -5 -6 -12 9 15 1 -12 -5 -6 7 9 15 -6 -12 -5 1 7 9 15 更多请参考此文,程序员编程艺术第二十七章:重新排列数组(不改变相对顺序&时间O(N)&空间O(1),半年未被KO)http://blog.csdn.net/v_july_v/article/details/7329314。 ================================================ FILE: 9 Algorithms Job Interview/5.2 数列-nsum问题.md ================================================ #数列-nsum问题 ## 数组,找出和为 s 的两个数 暴力解法 ``` class Solution { public int[] twoSum(int[] nums, int target) { int res[] = new int[2]; for(int i = 0; i < nums.length; i++){ for(int j = i + 1; j < nums.length; j++){ if(nums[i] + nums[j] == target){ res[0] = i; res[1] = j; break; } } } return res; } } ``` 使用 hash表,2遍扫描 ``` Java class Solution { public int[] twoSum(int[] nums, int target) { int res[] = new int[2]; Map map= new HashMap<>(); for(int i = 0; i < nums.length; i++){ map.put(nums[i], i); } for(int i = 0; i < nums.length; i++){ int temp = target - nums[i]; if(map.containsKey(temp) && map.get(temp) != i){ res[0] = map.get(temp); res[1] = i; } } return res; } } ``` ## 找出和为N+1的2个数 一个整数数列,元素取值可能是`1~N`(N是一个较大的正整数)中的任意一个数,相同数值不会重复出现。 设计一个算法,找出数列中符合条件的数对的个数,满足数对中两数的和等于`N+1`。 复杂度最好是 `O(n)` ,如果是 `O(n2)`则不得分。 分析:列出所有的数对,如输入15,输出`【1,14】【2,13】【3,12】。。。` ``` int print_sequence_sum(int n) ``` ## 找出和为m的2个数 输入一个已经按升序排序过的数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。 要求时间复杂度是O(n)。如果有多对数字的和等于输入的数字,输出任意一对即可。 例如输入数组【1、2、4、7、11、15=和数字15。由于4+11=15,因此输出4和11。 ``` //返回0找到,返回-1没找到 int findaddends(int *data,int length,int sum,int *a,int *b); ``` 分析: 数组升序排列,查找可用二分查找,时间复杂度`O(logn)`,这样问题就变成了找其中一个加数的问题,复杂度为`N*logN` 巧妙解法:数组2端向中间扫描,复杂度O(N) ## 求子数组的最大和(最大字段和) 输入一个整形数组,数组里有正数也有负数。 数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。 求所有子数组的和的最大值。要求时间复杂度为O(n)。 例如输入的数组为`1, -2, 3, 10, -4, 7, 2, -5` ,和最大的子数组为`3, 10, -4, 7, 2` ,因此输出为该子数组的和18。 1. 蛮力法 fmax(i,j) 找出最大的值,3重循环 ,复杂度 0(n^3) 2. `动态规划` maxendindhere保存当前累加的和,如果<0, 就把maxendinghere清零 , max保存最终的最大和; 如果都是负数 ``` int maxSumOfVector(int *data,int length){ int max=0; int maxendinghere = 0; if(data==NULL || length<=0){ return 0; } for(int i=0;i < length, i++){ maxendinghere+=data[i]; if(maxendinghere<0){ maxendinghere=0; continue; } if(maxendinghere>max){ max=maxendinghere; } } return max; } ``` ## 和为n连续正数序列 题目:输入一个正数n,输出所有和为n连续正数序列。 例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以输出3个连续序列1-5、4-6和7-8。 ``` print_continuous_sequence_sum(int n) ``` 思路一:枚举法。从1开始一直加到等于n,再从2开始。。一直到 n/2+1,在每一轮,都要逐步比较。复杂度`O(N^2)` 思路二: a[small,big] sum[small,big]>N small往前移动,否则,big往前移动。 `O(N)` 复杂度搞定 ## 求连续子数组和为m的组合 数组 [2,3,1,2,4,3]求连续子数组sum=7, 结果[1,2,4] [3,4] * 暴力遍历 * 滑动窗口, 左指针,右指针 ``` if sum= 0 && index_b >= 0) { //2个指针都有值时 if (a[index_a] >= 0 && b[index_b] >= 0) { if (a[index_a] > b[index_b]) { a[len_a--] = a[index_a--]; }else{ a[len_a--] = b[index_b--]; } } //a 无值,b有值 ; 把剩下b放好 if (index_a < 0 && index_b >= 0) { while (index_b >= 0) { a[len_a--] = b[index_b--]; } } //a 有值,b无值; 把剩下a放好 if (index_a >=0 && index_b < 0) { while (index_b >= 0) { } } } } ``` ## 两个序列和之差最小 有两个序列a,b,大小都为n,序列元素的值任意整数,无序; 要求:通过交换a,b中的元素,使[序列a元素的和]与[序列b元素的和]之间的差最小。 例如: ``` var a=[100,99,98,1,2, 3]; var b=[1, 2, 3, 4,5,40]; ``` ================================================ FILE: 9 Algorithms Job Interview/5.4 数列-查找.md ================================================ # 数列-查找 ## 数组中超过出现次数超过一半的数字 题目:数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。 `+-1 计数法` ## 找数组里面重复的一个数 找数组里面重复的一个数,一个含n个元素的整数数组至少存在一个重复数,请编程实现,在O(n)时间内找出其中任意一个重复数。 1. hash算法,空间要求多 (注意数组元素要是int类型),数的大小范围和数组长度N都可以是无穷大,这里使用hash算法,空间复杂度是O(n),空间可能无法满足条件。 2. 先排序,复杂度n(logn);然后遍历一遍就可以知道哪些数重复了。 2. 高级解法: 将问题转化为 `判断单链表中存在环` 类似问题:找出数组中唯一的重复元素 ## 查找只出现一次的元素 给一个非空整数数组,比如 [2, 2, 3] ,其余元素均出现2次,找出那个只出现一次的元素。 思路: 异或运算,成对儿的数字就会变成 0,落单的数字和 0 做异或还是它本身 ``` int singleNumber(vector& nums) { int res = 0; for (int n : nums) { res ^= n; } return res; } ``` ## 数组中找出某数字出现的次数 在排序数组中,找出给定数字的出现次数。比如 [1, 2, 2, 2, 3] 中2的出现次数是3次。 分析: 1. 因为是排序数组,可以使用二分查找 2. 将二分查找坚持到底,这样在最坏的情况下([2,2,2,2,2,2,2])都有0(lgn)复杂度 ``` int binary_search_first(int *a,int length,int key); int binary_search_lash(int *a,int length,int key); ``` ## 大于K的最小正整数 给定一个集合A=[0,1,3,8](该集合中的元素都是在0,9之间的数字,但未必全部包含),指定任意一个正整数K,请用A中的元素组成一个大于K的最小正整数。 比如,A=[1,0] K=21 那么输出结构应该为100。 ## 查找最小的k个元素(top-k) 题目:输入n个整数,输出其中最小的k个。 例如输入1,2,3,4,5,6,7和8这8个数字,则最小的4个数字为1,2,3和4。 ``` topMinK(int *a,int length,int k); topMaxK(int *a,int length, int k); ``` 1. 全部排序 复杂度 NlogN , 数据了较大时,内存可能承受不住 2. 部分排序 维护一个大小为K的数组,由大到小排序,然后遍历所有数据,每个数据跟数组中最小元素比较,如果比最小元素大,就要插入数组了,这里还有寻找插入位置,移动数组元素的cpu消耗。复杂度是N*K 3. 堆排序 。在这的K较大时(比如这道题目:2亿个整数中求最大的100万之和),上面的算法还是有很多可以改进的地方,如采用二分查找定位插入位置,移动数组元素的计算是躲不过去了。那有没有什么数据结构即能`快速查找,还能快速移动元素`呢?最好是O(1)复杂度。 答案就是`二叉堆`。我们可以遍历总量中的每个元素来跟二叉堆中的堆顶元素比较(堆顶元素在`小根堆`中最小值,在`大根堆`中是最大值),这样在0(1)复杂度就可以完成查找操作,揭下来需要的操作就是重新调整推结构,复杂度是O(logk),因此整个操作复杂度是 `O(n*logk)` `top-k 小的时候用 *大根堆* ,top-k 大得时候用 *小根堆*` ## 找第k大的数 比如 `1,2,3,4,5,6,7,8 `这 8个数字,第3大的数字是 6 ``` int topK(int * a, int length, int k) ``` ```Java //冒泡实现 public int findK(int[] nums, int k){ //base case if (nums== null || nums.length < k) { return -1; } for(int i=1; i<=k; i++){ for(int j=0; j nums[next]){ int tmp = nums[j]; nums[j] = nums[next]; nums[next] = tmp; } } } return nums[nums.length-k]; } ``` ```Java /** * 小根堆实现 */ public static int findMaxK(int[] nums, int k) { PriorityQueue pq = new PriorityQueue<>(k, (a, b) -> (a-b) ); for (int i = 0; i 是n个不同的实数的序列,L的递增子序列是这样一个子序列 Lin=< aK1,ak2,…,akm >,其中k1< k2<…< km且aK1< ak2<…< akm。 求最大的m值。 如【5,6,7,3,2,8】 最长子序列 【5,6,7,8】 `动态规划` ## 在从1到n的正数中1出现的次数 题目:输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。 例如输入12,从1到12这些整数中包含1 的数字有1,10,11和12,1一共出现了5次。 分析:这是一道广为流传的google面试题。 ``` int one_appear_count(int n); ``` 思路1. 遍历`1~n`,统计出现1的个数;n足够大时,效率很低 思路2. 分析规律 ## 搜索旋转排序数组 整数数组 nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。 给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。 示例 1: ``` 输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4 ``` 示例 2: ``` 输入:nums = [4,5,6,7,0,1,2], target = 3 输出:-1 ``` ================================================ FILE: 9 Algorithms Job Interview/6 矩阵.md ================================================ # 矩阵 矩阵在计算机中表示就是二维数组。这部分内容都是有关二维数组和矩阵相关的题目。 ## 顺时针打印矩阵 题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。 例如:如果输入如下矩阵: ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ``` 则依次打印出数字`1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10` 。 分析:包括Autodesk、EMC在内的多家公司在面试或者笔试里采用过这道题。 难点: 各种边界条件判断,很容易搞错 一圈一圈打印,第一圈origin是(0,0),第二圈是(1,1) ``` void print_matrix(int **matrix,int rows,int cols); //或 void print_matrix(int *matrix,int rows,int cols); ``` 示例代码: ``` void print_matrix(int *matrix,int rows,int cols){ if(matrix==NULL) return ; if(rows<=0 || cols<=0) return; int start = 0; while(start*2=start;i--){ printf("\d ",matrix[start*rows+i]); } for(int i=endY-1;i>start;i--){ printf("\d ",matrix[i*rows+start]); } } ``` 测试代码见 print_matrix.c ## 从小到大输出矩阵的值 思路1:采用归并进行排序然后进行顺序打印 思路2:用n个指针指向每一行的第一个数,比较n个指针的数,打印最小的,然后指针后移,若无下一位,则赋值为null,直至所有数都对印完毕 ``` void printArry(int *a,int rows,int columns) { if(a==null) return; int *arr=new int[rows*(columns+1)];//添加看门狗值,INT_MAX for(int i=0;iarr[index]) { min=arr[index]; mini=i; } } if(min==INT_MAX) break;//表示都达到了看门处,则跳出循环。 cout< Google 面试题, 此题采用bfs和拓扑排序均可达到面试官的要求。笔者认为,一般的bfs可以达到hire;记忆化搜索和拓扑排序可以达到strong hire ================================================ FILE: 9 Algorithms Job Interview/7 二叉树.md ================================================ # 二元树 问题涉及有: * 遍历 * 翻转 * 子树 写二叉树的算法题,都是基于递归框架的。我们先要搞清楚 root 节点它自己要做什么,然后根据题目要求选择使用前序,中序,后续的递归框架。 二元树的结点定义如下: ``` struct SBinaryTreeNode // a node of the binary tree { int m_nValue; // value of node SBinaryTreeNode *m_pLeft; // left child of node SBinaryTreeNode *m_pRight; // right child of node }; ``` 二叉树递归框架 ```Java /* 二叉树遍历框架 */ void traverse(TreeNode root) { // 前序遍历 traverse(root.left) // 中序遍历 traverse(root.right) // 后序遍历 } ``` ## 二叉树翻转 输入一颗二元查找树,将该树转换为它的镜像,即在转换后的二元查找树中,左子树的结点都大于右子树的结点。用递归和循环两种方法完成树的镜像转换。例 如输入: ``` 8 / \ 6 10 /\ / \ 5 7 9 11 ``` 输出: ``` 8 / \ 10 6 /\ /\ 11 9 7 5 ``` 思路:二叉树翻转 , 把二叉树上的每一个节点的左右子节点进行交换 ``` // 将整棵树的节点翻转 TreeNode invertTree(TreeNode root) { // base case if (root == null) { return null; } /**** 前序遍历位置 ****/ // root 节点需要交换它的左右子节点 TreeNode tmp = root.left; root.left = root.right; root.right = tmp; // 让左右子节点继续翻转它们的子节点 invertTree(root.left); invertTree(root.right); return root; } ``` ## 把二元查找树转变成排序的双向链表 难度:中等 输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表。要求`不能创建任何新的结点`,只调整指针的指向。 ``` 10 / \ 6 14 /\ / \ 4 8 12 16 ``` 转换成双向链表 `4=6=8=10=12=14=16` 分析:题目要求不能创建任何节点,也就是只能调整各个节点的指向 思路一: ## 二叉树两个结点的最低共同父结点 (最近公共祖先) 设计一个算法,找出二叉树上任意两个结点的最近共同父结点。复杂度如果是O(n2)则不得分 输入二叉树中的两个结点,输出这两个结点在数中最低的共同父结点。 分析:求数中两个结点的最低共同结点是面试中经常出现的一个问题。这个问题至少有两个变种。 ``` TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { // base case if (root == null) return null; if (root == p || root == q) return root; TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); // 情况 1 :p, q 在 root 为根的树中 if (left != null && right != null) { return root; } // 情况 2 :p, q 不在 在 root 为根的树中 if (left == null && right == null) { return null; } // 情况 3 : p, q 只有1个在root 为根的树中 return left == null ? right : left; } ``` ## 求一个二叉树中任意两个节点间的最大距离 两个节点的距离的定义是 这两个节点间边的个数,比如某个孩子节点和父节点间的距离是1,和相邻兄弟节点间的距离是2,优化时间空间复杂度。 ## 在二元树中找出和为某一值的所有路径 题目:输入一个整数和一棵二元树。从树的根结点开始往下访问一直到叶结点所经过的所有结点形成一条路径。打印出和与输入整数相等的所有路径。 例如 输入整数22和如下二元树 ``` 10 / \ 5 12 /\ 4 7 ``` 则打印出两条路径:`10, 12` 和 `10, 5, 7` 。 1. 累加当前节点,累加和大于给定值 2. 不为叶节点,左子树入栈 ## 一棵排序二叉树,令 f=(最大值+最小值)/2,设计一个算法,找出距离f值最近、大于f值的结点。 复杂度如果是O(n2)则不得分。 ================================================ FILE: 9 Algorithms Job Interview/7.1 二叉树-遍历.md ================================================ # 二叉树-遍历 > 快速排序就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历 ## 2种方法实现二叉树的前序遍历。 递归和非递归 ## 按层打印二叉树 输入一颗二元树,从上往下按层打印树的每个结点,同一层中按照从左往右的顺序打印。 例如输入 ``` 8 /\ 6 10 / \ /\ 5 7 9 11 ``` 输出`8 6 10 5 7 9 11` ## 二元树的深度 题目:输入一棵二元树的根结点,求该树的深度.从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 例如:输入二元树: ``` 10 / \ 6 14 / / \ 4 12 16 ``` 输出该树的深度3。 分析:这道题本质上还是考查二元树的遍历。对于一颗完全二叉树,要求给所有节点加上一个pNext指针,指向同一层的相邻节点;如果当前节点已经是该层的最后一个节点,则将pNext指针指向NULL;给出程序实现,并分析时间复杂度和空间复杂度。 ## 二元树的最小深度 例如:输入二元树: ``` 10 / \ 6 14 / \ 12 16 ``` 输出该树的最小深度2。 ## 完全二叉树的节点个数 ## 判断整数序列是不是二元查找树的后序遍历结果 题目:输入一个整数数组,判断该数组是不是某二元查找树的后序遍历的结果。 如果是返回true,否则返回false。 例如输入`5、7、6、9、11、10、8`,由于这一整数序列是如下树的后序遍历结果: ``` 8 / \ 6 10 /\ / \ 5 7 9 11 ``` 因此返回true。如果输入7、4、6、5,没有哪棵树的后序遍历的结果是这个序列,因此返回false。 ================================================ FILE: 9 Algorithms Job Interview/7.2 二叉树-建树.md ================================================ # 二叉树-建树 ## 把一个有序整数数组放到二叉树中? 分析:本题考察二叉搜索树的建树方法,简单的递归结构。 关于树的算法设计一定要联想到递归,因为树本身就是递归的定义。而,学会把递归改称非递归也是一种必要的技术。 毕竟,递归会造成栈溢出,关于系统底层的程序中不到非不得以最好不要用。但是对某些数学问题,就一定要学会用递归去解决。 ## 恢复树结构 有一棵树(树上结点为字符串或者整数),请写代码将树的结构和数据写到一个文件中,并能通过读取该文件恢复树结构。 ================================================ FILE: 9 Algorithms Job Interview/7.5 二叉搜索树.md ================================================ # 二叉搜索树 关于二叉查找树特性[参考这里](../4%20Tree/2-二叉查找树/二叉查找树.md) ## 验证二叉搜索树 ## 二叉搜索树中的节点搜索 ## 二叉搜索树中的插入操作 ## 删除二叉搜索树中的节点 ================================================ FILE: 9 Algorithms Job Interview/7.9 树.md ================================================ # 树 ## 给一棵树 求最大宽度 ## 树的广度遍历深度遍历 ## 多叉树的层序遍历 ================================================ FILE: 9 Algorithms Job Interview/8 图.md ================================================ # 图 深度优先遍历(DFS)和广度优先遍历(BFS) 深度优先遍历(Depth First Search, 简称 DFS) 与广度优先遍历(Breath First Search)是图论中两种非常重要的算法,生产上广泛用于拓扑排序,寻路(走迷宫),搜索引擎,爬虫等,也频繁出现在 leetcode,高频面试题中 ## 深度优先遍历 深度优先遍历(DFS) ## 广度优先遍历 广度优先遍历(BFS) ## 华为面试题:一类似于蜂窝的结构的图,进行搜索最短路径(要求5分钟) ## 求一个有向连通图的割点,割点的定义是,如果除去此节点和与其相关的边, 有向图不再连通,描述算法。 ## 平面上N个点,每两个点都确定一条直线, 求出斜率最大的那条直线所通过的两个点(斜率不存在的情况不考虑)。时间效率越高越好。 ================================================ FILE: 9 Algorithms Job Interview/9 智力思维训练.md ================================================ # 智力题 这部分内容的题目侧重思维发散。 1.有两个房间,一间房里有三盏灯,另一间房有控制着三盏灯的三个开关,这两个房间是 分割开的,从一间里不能看到另一间的情况。 现在要求受训者分别进这两房间一次,然后判断出这三盏灯分别是由哪个开关控制的。 有什么办法呢? 2.你让一些人为你工作了七天,你要用一根金条作为报酬。金条被分成七小块,每天给出一块。如果你只能将金条切割两次,你怎样分给这些工人? 有4张红色的牌和4张蓝色的牌,主持人先拿任意两张,再分别在A、B、C三人额头上贴任意两张牌, A、B、C三人都可以看见其余两人额头上的牌,看完后让他们猜自己额头上是什么颜色的牌, A说不知道,B说不知道,C说不知道,然后A说知道了。 请教如何推理,A是怎么知道的。 如果用程序,又怎么实现呢? 有A、B、C、D四个人,要在夜里过一座桥。他们通过这座桥分别需要耗时1、2、5、10分钟,只有一支手电,并且同时最多只能两个人一起过桥。请问,如何安排,能够在17分钟内这四个人都过桥? 有12个小球,外形相同,其中一个小球的质量与其他11个不同,给一个天平,问如何用3次把这个小球找出来,并且求出这个小球是比其他的轻还是重 “十袋白色粉末,其中有一袋溶于水两分钟以后会变蓝,现在只有四个杯子,无限多的水,要求是在最短的时间内找出这袋特殊的粉末” 一二三四号粉末放第1个杯子,二三四号粉末分别放在第234个杯子里,五六七号粉末放到2号杯子里,六七号粉末分别放到34号杯子里,八九号粉末放到第3个杯子里,九十号粉末放到第4个杯子里,OK了” 27.跳台阶问题 题目:一个台阶总共有n级,如果一次可以跳1级,也可以跳2级。 求总共有多少总跳法,并分析算法的时间复杂度。 这道题最近经常出现,包括MicroStrategy等比较重视算法的公司都 曾先后选用过个这道题作为面试题或者笔试题。 1.设计一个魔方(六面)的程序。 1.用天平(只能比较,不能称重)从一堆小球中找出其中唯一一个较轻的,使用x次天平,最多可以从y个小球中找出较轻的那个,求y与x的关系式 26、有一根27厘米的细木杆,在第3厘米、7厘米、11厘米、17厘米、23厘米这五个位置上各有一只蚂蚁。 木杆很细,不能同时通过一只蚂蚁。开始时,蚂蚁的头朝左还是朝右是任意的,它们只会朝前走或调头,但不会后退。 当任意两只蚂蚁碰头时,两只蚂蚁会同时调头朝反方向走。假设蚂蚁们每秒钟可以走一厘米的距离。 编写程序,求所有蚂蚁都离开木杆的最小时间和最大时间。 20、13个球一个天平,现知道只有一个和其它的重量不同,问怎样称才能用三次就找到那个球?(http://zhidao.baidu.com/question/66024735.html )。 1.烧一根不均匀的绳,从头烧到尾总共需要1个小时。 现在有若干条材质相同的绳子,问如何用烧绳的方法来计时一个小时十五分钟呢? 2.你有一桶果冻,其中有黄色、绿色、红色三种,闭上眼睛抓取同种颜色的两个。 抓取多少个就可以确定你肯定有两个同一颜色的果冻?(5秒-1分钟) 3.如果你有无穷多的水,一个3公升的提捅,一个5公升的提捅,两只提捅形状上下都不均匀, 问你如何才能准确称出4公升的水?(40秒-3分钟) 一个岔路口分别通向诚实国和说谎国。 来了两个人,已知一个是诚实国的,另一个是说谎国的。 诚实国永远说实话,说谎国永远说谎话。现在你要去说谎国, 但不知道应该走哪条路,需要问这两个人。请问应该怎么问?(20秒-2分钟) 100.第4组微软面试题,挑战思维极限 1.12个球一个天平,现知道只有一个和其它的重量不同,问怎样称才能用三次就找到那个球。 13个呢?(注意此题并未说明那个球的重量是轻是重,所以需要仔细考虑)(5分钟-1小时) 2.在9个点上画10条直线,要求每条直线上至少有三个点?(3分钟-20分钟) 3.在一天的24小时之中,时钟的时针、分针和秒针完全重合在一起的时候有几次? 都分别是什么时间?你怎样算出来的?(5分钟-15分钟) 91. 1.一道著名的毒酒问题 有1000桶酒,其中1桶有毒。而一旦吃了,毒性会在1周后发作。 现在我们用小老鼠做实验,要在1周内找出那桶毒酒,问最少需要多少老鼠。 2.有趣的石头问题 有一堆1万个石头和1万个木头,对于每个石头都有1个木头和它重量一样, 把配对的石头和木头找出来。 92. 1.多人排成一个队列,我们认为从低到高是正确的序列,但是总有部分人不遵守秩序。 如果说,前面的人比后面的人高(两人身高一样认为是合适的), 那么我们就认为这两个人是一对“捣乱分子”,比如说,现在存在一个序列: 176, 178, 180, 170, 171 这些捣乱分子对为 <176, 170>, <176, 171>, <178, 170>, <178, 171>, <180, 170>, <180, 171>, 那么,现在给出一个整型序列,请找出这些捣乱分子对的个数(仅给出捣乱分子对的数目即可,不用具体的对) 要求: 输入: 为一个文件(in),文件的每一行为一个序列。序列全为数字,数字间用”,”分隔。 输出: 为一个文件(out),每行为一个数字,表示捣乱分子的对数。 详细说明自己的解题思路,说明自己实现的一些关键点。 并给出实现的代码 ,并分析时间复杂度。 限制: 输入每行的最大数字个数为100000个,数字最长为6位。程序无内存使用限制。 29、有A、B、C、D四个人,要在夜里过一座桥。他们通过这座桥分别需要耗时1、2、5、10分钟,只有一支手电,并且同时最多只能两个人一起过桥。请问,如何安排,能够在17分钟内这四个人都过桥? 30、有12个小球,外形相同,其中一个小球的质量与其他11个不同, 给一个天平,问如何用3次把这个小球找出来,并且求出这个小球是比其他的轻还是重 创新工场面试题:abcde五人打渔,打完睡觉,a先醒来,扔掉1条鱼,把剩下的分成5分,拿一份走了;b再醒来,也扔掉1条,把剩下的分成5份,拿一份走了;然后cde都按上面的方法取鱼。问他们一共打了多少条鱼,写程序和算法实现。提示:共打了多少条鱼的结果有很多。但求最少打的鱼的结果是3121条鱼(应该找这5个人问问,用什么工具打了这么多条鱼)。(http://blog.csdn.net/nokiaguy/article/details/6800209)。 我们有很多瓶无色的液体,其中有一瓶是毒药,其它都是蒸馏水,实验的小白鼠喝了以后会在5分钟后死亡,而喝到蒸馏水的小白鼠则一切正常。现在有5只小白鼠,请问一下,我们用这五只小白鼠,5分钟的时间,能够检测多少瓶液体的成分? 淘宝2012笔试(研发类):http://topic.csdn.net/u/20110922/10/e4f3641a-1f31-4d35-80da-7268605d2d51.html(一参考答案)。 华为面试2:1分2分5分的硬币,组成1角,共有多少种组合。 43、Smith夫妇召开宴会,并邀请其他4对夫妇参加宴会。在宴会上,他们彼此握手, 并且满足没有一个人同自己握手,没有两个人握手一次以上,并且夫妻之间不握手。 然后Mr. Smith问其它客人握手的次数,每个人的答案是不一样的。 求Mrs Smith握手的次数 44、有6种不同颜色的球,分别记为1,2,3,4,5,6,每种球有无数个。现在取5个球,求在一下 的条件下: 1、5种不同颜色, 2、4种不同颜色的球, 3、3种不同颜色的球, 4、2种不同颜色的球, 它们的概率。 45、有一次数学比赛,共有A,B和C三道题目。所有人都至少解答出一道题目,总共有25人。 在没有答出A的人中,答出B的人数是答出C的人数的两倍;单单答出A的人,比其他答出A的人 总数多1;在所有只有答出一道题目的人当中,答出B和C的人数刚好是一半。 求只答出B的人数。 12个高矮不同的人,排成两排,每排必须是从矮到高排列,而且第二排比对应的第一排的人高,问排列方式有多少种? 这个笔试题,很YD,因为把某个递归关系隐藏得很深. 47、金币概率问题(威盛笔试题) 题目:10个房间里放着随机数量的金币。每个房间只能进入一次,并只能在一个房间中拿金币。 一个人采取如下策略:前四个房间只看不拿。随后的房间只要看到比前四个房间都多的金币数, 就拿。否则就拿最后一个房间的金币。? 编程计算这种策略拿到最多金币的概率。 22、有5个海盗,按照等级从5到1排列,最大的海盗有权提议他们如何分享100枚金币。 但其他人要对此表决,如果多数反对,那他就会被杀死。 他应该提出怎样的方案,既让自己拿到尽可能多的金币又不会被杀死? (提示:有一个海盗能拿到98%的金币) 五只猴子分桃。半夜,第一只猴子先起来,它把桃分成了相等的五堆,多出一只。于是,它吃掉了一个,拿走了一堆; 第二只猴子起来一看,只有四堆桃。于是把四堆合在一起,分成相等的五堆,又多出一个。于是,它也吃掉了一个,拿走了一堆;.....其他几只猴子也都是 这样分的。问:这堆桃至少有多少个?(朋友说,这是小学奥数题)。 参考答案:先给这堆桃子加上4个,设此时共有X个桃子,最后剩下a个桃子.这样: 第一只猴子分完后还剩:(1-1/5)X=(4/5)X; 第二只猴子分完后还剩:(1-1/5)2X; 第三只猴子分完后还剩:(1-1/5)3X; 第四只猴子分完后还剩:(1-1/5)4X; 第五只猴子分完后还剩:(1-1/5)5X=(1024/3125)X; 得:a=(1024/3125)X; 要使a为整数,X最小取3125. 减去加上的4个,所以,这堆桃子最少有3121个。 已知有个rand7()的函数,返回1到7随机自然数,让利用这个rand7()构造rand10() 随机1~10。 (参考答案:这题主要考的是对概率的理解。程序关键是要算出rand10,1到10,十个数字出现的考虑都为10%.根据排列组合,连续算两次rand7出现的组合数是7*7=49,这49种组合每一种出现考虑是相同的。怎么从49平均概率的转换为1到10呢?方法是: 1.rand7执行两次,出来的数为a1=rand7()-1,a2=rand7()-1. 2.如果`a1*7+a2<40,b=(a1*7+a2)/4+1`;如果`a1*7+a2>=40`, 重复第一步。参考代码如下所示: ``` int rand7() { return rand()%7+1; } int rand10() { int a71,a72,a10; do { a71= rand7()-1; a72 = rand7()-1; a10 = a71 *7 + a72; } while (a10>= 40); return (a71*7+a72)/4+1; } ``` 2)一串首尾相连的珠子(m个),有N种颜色(N<=10), 设计一个算法,取出其中一段,要求包含所有N中颜色,并使长度最短。 并分析时间复杂度与空间复杂度。 41.求固晶机的晶元查找程序 晶元盘由数目不详的大小一样的晶元组成,晶元并不一定全布满晶元盘, 照相机每次这能匹配一个晶元,如匹配过,则拾取该晶元, 若匹配不过,照相机则按测好的晶元间距移到下一个位置。 求遍历晶元盘的算法 求思路。 ================================================ FILE: 9 Algorithms Job Interview/91 系统设计.md ================================================ # 系统设计 ## 要求设计一个DNS的Cache结构,要求能够满足每秒5000以上的查询,满足IP数据的快速插入,查询的速度要快。 ## 设计一个系统处理词语搭配问题 设计一个系统处理词语搭配问题,比如说 中国 和人民可以搭配, 则中国人民 人民中国都有效。要求: * 系统每秒的查询数量可能上千次; * 词语的数量级为10W; * 每个词至多可以与1W个词搭配 当用户输入中国人民的时候,要求返回与这个搭配词组相关的信息。 ## 任务调度 系统有很多任务,任务之间有依赖,比如B依赖于A,则A执行完后B才能执行 (1)不考虑系统并行性,设计一个函数`(Task *Ptask,int Task_num)`不考虑并行度,最快的方法完成所有任务。 (2)考虑并行度,怎么设计 ``` typedef struct{ int ID; int * child; int child_num; }Task; ``` 提供的函数: ``` bool doTask(int taskID);无阻塞的运行一个任务; int waitTask(int timeout);返回运行完成的任务id,如果没有则返回-1; bool killTask(int taskID);杀死进程 ``` ## 设计一种内存管理算法 相关的问题: * 请编写实现malloc()内存分配函数功能一样的代码 * 用C语言实现函数memmove()函数 ``` void * memmove(void *dest, const void *src, size_t n) ``` memmove()函数的功能是拷贝src所指的内存内容前n个字节到dest所指的地址上。 分析:由于可以把任何类型的指针赋给void类型的指针,这个函数主要是实现各种数据类型的拷贝。 ## A向B发邮件,B收到后读取并发送收到,但是中间可能丢失了该邮件,怎么设计一种最节省的方法,来处理丢失问题。 ## 设计一种算法求出算法复杂度 ================================================ FILE: 9 Algorithms Job Interview/97 其他.md ================================================ # 其他 ## 两个圆相交 两个圆相交,交点是A1,A2。现在过A1点做一直线与两个圆分别相交另外一点B1,B2。B1B2可以绕着A1点旋转。问在什么情况下,B1B2最长 ## 输入四个点的坐标,求证四个点是不是一个矩形 关键点: 1.相邻两边斜率之积等于-1, 2.矩形边与坐标系平行的情况下,斜率无穷大不能用积判断。 3.输入四点可能不按顺序,需要对四点排序。 ## 排列组合问题 ## 1,2,3,4,5 五个不同的数字,打印不同的排列。这就是一个无向图的遍历,把每个数字看成一个节点。 ## 用1、2、2、3、4、5这六个数字,写一个main函数,打印出所有不同的排列, 如:512234、412345 等,要求:"4"不能在第三位,"3"与"5"不能相连. 这是对上一题增加难度。但是需要 1. 去掉 3,5之间的联通 2. 2重复,过滤重复结果 treeset 3. 4不能在3位 ## 圆形和正方形是否相交 用最简单, 最快速的方法计算出下面这个圆形是否和正方形相交 3D坐标系 原点(0.0,0.0,0.0) 圆形: 半径r = 3.0 圆心o = (*.*, 0.0, *.*) 正方形: 4个角坐标; 1:(*.*, 0.0, *.*) 2:(*.*, 0.0, *.*) 3:(*.*, 0.0, *.*) 4:(*.*, 0.0, *.*) 分析: 2个形状不相交: ================================================ FILE: 9 Algorithms Job Interview/README.md ================================================ # 面试题 这部分内容是算法问题合集,题目大多来自网络和书籍。我做了下简单的整理,很多题做了一些思路标记。 ## 小结 面试中如何更好的写算法题 * ide 工具 & 文本框 ;文本框环境写算法是通常没有智能提示,有些基础数据结构可能要自己定义 * 语言基础语法:各常用集合的api,要能够默写 * 题目:熟悉各种题目,避免在题目理解上花太多时间跟面试官拉齐 * 编程思路和框架:拿到题目以后能快速套思路和代码框架 * 编程细节:多练习,总结 ## 常用解题套路工具 * 数组,字符串问题:二分查找、 快慢指针、 左右指针、 滑动窗口、 前缀和数组、 差分数组。 * 二叉树问题:递归 * [动态规划问题](../8%20Algorithms%20Analysis/动态规划.md) * [常见算法题目Java实现](https://github.com/nonstriater/deep-in-java/tree/master/src/main/java/com/nonstriater/deepinjava/algo) ## 刷题框架套路 ### 遍历 ``` # 数组遍历框架 void traverse(int[] arr) { for (int i = 0; i < arr.length; i++) { // 迭代访问 arr[i] } } # 链表遍历框架 void traverse(ListNode head) { ListNode p = head; while(p.next != null) ... p = p.next } ``` ### 递归 ```Java # 链表递归 void traverse(ListNode head) { // 递归访问 head.val traverse(head.next); } # 二叉树递归 void traverse(TreeNode root) { traverse(root.left); traverse(root.right); } # 多叉树递归 void traverse(TreeNode root) { for (TreeNode child : root.children) traverse(child); } # 图的递归:,用个布尔数组 visited 做标记就行了 ``` 比如,二叉树的最近公共祖先 ```Java TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { // base case if (root == null) return null; if (root == p || root == q) return root; TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); // 情况 1 :p, q 在 root 为根的树中 if (left != null && right != null) { return root; } // 情况 2 :p, q 不在 在 root 为根的树中 if (left == null && right == null) { return null; } // 情况 3 : p, q 只有1个在root 为根的树中 return left == null ? right : left; } ``` ### 二分查找 [查找算法](../7%20Search/README.md) ```Java // 1, 2, 2, 2, 2, 3 public int bsearch(int[] nums, int target){ if(nums == null) { return -1; } int low = 0; int high = nums.length;// 注意 while(low < high){ middle = (high + low ) / 2; if(nums[middle] < target){ low = middle + 1; } else if (nums[middle] == target) { high = middle;//继续向左早 } else { high = middle; // 注意 , 这里没有 -1 } } return low; } ``` ### 快慢指针 如下判断链表是否有环 ``` public static boolean hasCycle(LinkNode head) { if(head == null) { return false; } LinkNode p = head, q = head; while(p.next != null && p.next.next != null && q.next != null) { p = p.next.next; q = q.next; if(p == q) { return true; } } return false; } ``` ### 左右指针 快排中 挖坑(pivot) 排序 ```Java // pivot 选择 尾部节点, 代码写起来更加简单; 移动元素更方便 // 左右指针技巧 static int partition(int[] nums, int left, int right){ int pivot = nums[right];//选尾部节点作为 pivot int end = right; right--; while (left < right) { if (nums[left] <= pivot) { left ++ ; //左边指针 窗口变小 continue; } //元素比 pivot 大,右边指针 窗口变小 //swap left & right swap(nums, left, right); right--; } // 跟 pivot 元素置换 int i = 0; if (nums[left] <= pivot) { //swap left+1 & pivot i = left +1; } else {//swap left & pivot i = left ; } swap(nums, i, end); return i; } ``` ### 滑动窗口 双指针中有一类比较难的技巧就是`滑动窗口` 滑动窗口: 无重复字符的最长子串 ```Java //如 ”abcabbcbb“ 输出 3 public static int longestSubString(char[] s){ int left = 0, right = 0; int res = 0; // 记录最长结果 Map window = new HashMap(); while (right < s.length) { Character c = s[right]; //窗口变大 right++; window.put(c, window.getOrDefault(c, 0) + 1); //java写的麻烦,不一定记得这个api if (window.get(c) > 1) { //判断左侧窗口是否要收缩 char d = s[left]; left ++; window.put(d, window.get(d)-1); } // 在这里更新答案 res = res > (right-left) ? res: (right-left); } return res; } ``` ### 排序算法 [排序算法](../6%20Sort/README.md) ```Java #快排 quickSort void quicksort(int[] nums, int left, int right){ if (left= 'A' && *start <= 'Z'){//大写 if (*end >= 'a' && *end <= 'z'){ char tmp = *start; *start = *end; *end = tmp; start++; } end--; }else{//小写 if (*end >= 'A' && *end <= 'Z'){ end--; } start++; } } return str; } int main(int argc, char const *argv[]) { char test[] = "HaJKPnobAACPc";//"char *test" always make "28767 bus error" printf("%s\n", proc(test)); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/1 string/replce_blank.c ================================================ #include "stdio.h" char *replace_blank(char *source){ int count = 0; char *tail = source; if (source == NULL) return NULL; while(*tail != '\0'){ if (*tail == ' ') count++; tail++; } while(count){ if(*tail != ' '){ *(tail+2*count) = *tail; }else{ *(tail+2*count) = '0'; *(tail+2*count-1) = '2'; *(tail+2*count-2) = '%'; count--; } tail--; } return source; } int main(int argc, char const *argv[]) { char str[100]="we are happy"; printf("ret=%s\n",replace_blank(str)); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/1 string/revert_by_word.c ================================================ #include "stdio.h" void _reverse(char *start,char *end){ if ((start == NULL) || (end == NULL)) return; while(start < end){ char tmp = *start; *start = *end; *end = tmp; start++, end--; } } char *_revert_by_word(char *source){ char *end = source; char *start = source; while(*start != '\0'){ if (*start == ' '){ start++; end++; }else if(*end == ' ' || *end == '\0'){ _reverse(start,end-1); start = end; }else{ end++; } } return source; } char *revert_by_word(char *source){ char *end = source; char *start = source; if (source == NULL) return NULL; while (*end != '\0') end++; end--; /*整个句子逆序*/ _reverse(start,end); /*按单词逆序.tneduts a ma I */ _revert_by_word(source); return source; } //为啥运行时 bus error? int main(int argc, char const *argv[]) { char *test = "how are you ?"; printf("%s\n",_revert_by_word(test)); test = "I am a student."; printf("%s\n",revert_by_word(test)); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/1 string/string.c ================================================ #include "stdio.h" typedef int BOOL; #define YES 1 #define NO 0 // 反序 void revert(const char *source,char *dest){ const char *s = source; char *end = dest; while(*s){ *end++ = *s++; //先拷贝字符串 } *end='\0'; end--;// 指向最后一个字符 while(end>dest){ char tmp = *end; *end = *dest; *dest = tmp; end--; dest++; } } // 按单词逆向 如 this is a sentence 输出 sentence a is this // 先按单词逆序,再对整个句子逆序 void revertByWord(const char *src, char *dest){ // 先拷贝字符串到dest char *tmp = dest; char *dest_end; while(*src){ *tmp++ = *src++; } *tmp='\0'; dest_end = tmp-1; //这里留作下面用; tmp = dest; // 扫描一遍 dest,按单词逆序 char *start=dest-1,*end=dest; while(*tmp){ if (*tmp == ' ')//逆序 { start++; //对单词逆向 while(end>start){ *start = *start + *end; *end = *start - *end; *start = *start - *end; start++; end--; } start=tmp;// 如果句子中又多个连续空格,也会对空格做逆序,浪费cpu } tmp++; end=tmp-1; } if (!*tmp)//最后一个字符,对最后一个单词逆序 { start ++; while(end>start){ *start = *start + *end; *end = *start - *end; *start = *start - *end; start++; end--; } } tmp = dest; // 逆向整个句子 while(dest_end>tmp){ *tmp = *tmp + *dest_end; *dest_end = *tmp - *dest_end; *tmp = *tmp - *dest_end; tmp++; dest_end--; } } // 回文,或 对称 BOOL symmetrical(const char *scr,int length){ const char *end = scr+length-1; BOOL is=YES; while(end>scr){ if (*scr++ != *end--) { is=NO; } } return is; } //找出第一个只出现一次的字符,找不到就返回‘0’ char first_appear_only_once(const char *source){ // 建立hash表 const int length=256; int hash_table[length]; for (int i = 0; i < length; ++i) { hash_table[i]=0; } //第一次扫描,建立hash表 const char *tmp = source; while(*tmp){ hash_table[*tmp++]++; } // 第二次扫描 while(*source){ if(hash_table[*source]==1){ return *source; } source++; } // 没找到 if (!*source) { return '0'; } return '0'; } // 最大对称子串的长度 int max_symmetrical_char_length(const char *scr){ int max_symmetrical_length=1; return 0; } // 找最大数字子串 int continumax(char *outputstr,char *intputstr){ return 0; } // 最大连续递增数字串 int continuousnum(const char *input, char *output){ return 0; } // 2个字符串的最长公共子串 int longest_common_subsequence(const char *s1,const char *s2, char *common){ return 0; } // 找子串 char *strstr(const char *haystack, const char * needle){ return NULL; } // 子串个数 char *sub_str_count(const char *src, const char *substr, int *count){ return NULL; } // 删除子串 char *delete(char *source,const char *del,BOOL greedy){ return NULL; } // 字符串拷贝 char *strcpy(const char *src,char *dest){ char *tmp = dest; while(*src){ *dest++ = *src++; } *dest = '\0'; return tmp; } // aabcd & bcad , abc&bca BOOL isBrother(const char *first,const char *secd){ return YES; } // 字符串就地压缩 如 “aaabcc” “a3bc2” /* 注意连续字符大于9的情况 */ void compress(char *src){ if (*src == NULL) { return; } int count = 1; char *back=src,*forward=src+1; while(*forward){ count = 1; if (*forward == *back) { count++; forward++; }else{ // if (count!=1) { back++; *back=count; } } } *++back = '\0'; } // 删除其中的数字并压缩,2个指针,一前一后 void filternum(char *src){ if (!*src) { return; } char *back=src ,*forward=src; while(*forward){ if (*forward >= '0' && *forward <= '9') { forward++; }else{ *back++ = *forward++; } } *back='\0'; // 局部变量释放,不能作为返回值 } // 删除字符串中的数字,不改变原串,同上 char *filternumber(const char*src,char *dest){ return NULL; } // 删除指定字符,不开辟空间 void delete_character(char *src , char target){ char *back=src,*forward = src; while(*forward){ if (*forward == target) { forward++; }else{ *back++ = *forward++; } } *back = '\0'; } // 统计文件中单词的出现的个数 void statistics(FILE *fd){ } // 字符串中字符的所有排列 int main(int argc, char const *argv[]) { // 反转 char *reverse = "nonstriater"; char reversed[20]; revert(reverse,reversed); printf("%s after reverse : %s\n", reverse,reversed); // 按单词逆序 char *sentence = "this is a sentence!"; // "this is a sentence" char dest[50]; revertByWord(sentence,dest); printf("%s revert by word: %s\n",sentence,dest ); // 回文 char symmetri[] = "dacbcacbcad"; printf("%s : %d\n", symmetri, symmetrical(symmetri,sizeof(symmetri)-1) ); char unsymmetrical[] = "aaacbdaa"; printf("%s : %d\n", unsymmetrical, symmetrical(unsymmetrical,sizeof(unsymmetrical)-1) ); // 第一次出现一次的字符 printf("%s第一次出现一次的字符是:%c\n",symmetri,first_appear_only_once(symmetri) );// '0' printf("%s第一次出现一次的字符是:%c\n",unsymmetrical,first_appear_only_once(unsymmetrical) ); // 'c' // 过滤数字 char *charnumber = "this is 58tongcheng12haha!"; //filternum(charnumber); printf(" after filter: %s\n", charnumber ); // 过滤指定字符 //delete_character(charnumber,'i'); // 居然会出 bus error!!!!!???? printf("after filter:%s\n", charnumber); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/10.c ================================================ #include "stdio.h" void generate(int a,int b,int N ,int * Q){ int p=1,q=1; int tmpA,tmpB; for (int i = 0; i < N; ++i) { tmpA = a*p; tmpB = b*q; if (tmpA>tmpB) { Q[i]=tmpB; q++; } else if(tmpA #include #define PrintInt(expr) printf("%s : %d\n",#expr,(expr)) int main() { int y = 100; int *p; p = malloc(sizeof(int)); *p = 10; y = y / *p; PrintInt(y); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/21.c ================================================ #include int main() { int day,month,year; printf("Enter the date (dd-mm-yyyy) format including -'s:"); scanf("%d-%d-%d",&day,&month,&year); printf("The date you have entered is %d-%d-%d\n",day,month,year); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/22.c ================================================ #include int main() { int n; printf("Enter a number:\n"); scanf("%d",&n); // & printf("You entered %d \n",n); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/23.c ================================================ #include int main() { int cnt = 5, a; do { a /= cnt; } while (cnt --); printf ("%d\n", a); return 0; } /// cnt==1 时,会发生除0错误 ================================================ FILE: 9 Algorithms Job Interview/codes/24.c ================================================ #include int main() { int i = 6; if( ((++i < 7) && ( i++/6)) || (++i <= 9)) ; printf("%d\n",i); return 0; } // && 优先级大于 || ================================================ FILE: 9 Algorithms Job Interview/codes/25.c ================================================ #include #include #define SIZE 15 int main() { int *a, i; a = malloc(SIZE*sizeof(int)); for (i=0; i int main() { char dummy[80]; printf("Enter a string:\n"); scanf("%[^a]",dummy); printf("%s\n",dummy); return 0; } /** 输入一个串,以字符'a'结尾 */ ================================================ FILE: 9 Algorithms Job Interview/codes/27.c ================================================ #include int main() { int a=3, b = 5; printf(&a["Ya!Hello! how is this? %s\n"], &b["junk/super"]); //printf(3["helloworld \n"]); printf("%c\n", 0["this"] ); printf(&a["WHAT%c%c%c %c%c %c !\n"], 1["this"], 2["beauty"],0["tool"],0["is"],3["sensitive"],4["CCCCCC"]); return 0; } /* */ ================================================ FILE: 9 Algorithms Job Interview/codes/28.c ================================================ #include #include int main() { int* ptr1,*ptr2; ptr1 = malloc(sizeof(int)); ptr2 = ptr1; *ptr2 = 10; return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/4 numer/Power.c ================================================ #include "stdio.h" double Power(double base, int exponent) { if (exponent == 0) return 1; if (exponent == 1) return base; double result = Power(base, exponent >> 1); result *= result; if (exponent & 1) result = result*base; return result; } int main(int argc, char const *argv[]) { printf("%f\n", Power(2,4)); printf("%f\n", Power(2.1,4)); printf("%f\n", Power(0,4)); printf("%f\n", Power(2,0)); printf("%f\n", Power(2,-3)); //负数就挂了 return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/4 numer/integer_to_bin.c ================================================ #include "stdio.h" #include "stdlib.h" char *integer_to_bin(long integer){ int bit_num = sizeof(long)*8; char *buffer= (char *)malloc(bit_num+1); if (buffer==NULL) { return "malloc error"; } buffer[bit_num]='\0'; for (int i = 0; i < bit_num; ++i) { buffer[i]= integer<>(bit_num-1); buffer[i]= buffer[i]==0?'0':'1'; } return buffer; } char *integer_to_hex(long integer){ int bit_num=sizeof(long)*8/4; char *buffer=(char *)malloc(bit_num+3); buffer[0]='0'; buffer[1]='x'; buffer[bit_num+2]='\0'; char *tmp=&buffer[2]; for (int i = 0; i < bit_num; i++) { tmp[i]= integer<<(i*4)>>(bit_num*4-4); tmp[i]= tmp[i]>=0?tmp[i]:tmp[i]+16; tmp[i]= tmp[i]<10?(tmp[i]+48):(tmp[i]-10+'A'); } return buffer; } /* A 65 0 48 */ int main(){ printf("b(11) = %s\n", integer_to_bin(11)); printf("b(1023) = %s\n",integer_to_bin(1023) ); printf("b(23564) = %s\n",integer_to_bin(23564) ); printf("h(11) = %s\n", integer_to_hex(11)); printf("h(1023) = %s\n",integer_to_hex(1023) ); printf("h(23564) = %s\n",integer_to_hex(23564) ); printf("b(11) = %x\n", 11); printf("b(1023) = %x\n",1023 ); printf("b(23564) = %x\n",23564 ); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/4 numer/isSquare.c ================================================ #include "stdio.h" int isSquare(unsigned integer){ if (integer==1 || integer==0) { return integer; } int divider=2,result=integer/divider; while(result>divider){ divider++; result = integer/divider; } if (result!=divider) { return -1; } return result; } int main(int argc, char const *argv[]) { // 243,1849(43),289(17), 64(8) int a[] = {1849,144,243,289,64,1194877489}; for (int i = 0; i < sizeof(a)/sizeof(int); ++i) { int ret = isSquare(a[i]); if (ret==-1) { printf("%d 不是某个数的平方\n",a[i]); }else{ printf("%d 是 %d 的平方\n", a[i], ret); } } return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/4 numer/one_appear_count_by_binary.c ================================================ #include "stdio.h" int one_appear_count_by_binary(int num){ int count = 0; while(num !=0 ){ num &= num-1; count++; } return count; } int main(int argc, char const *argv[]) { int test = 10; printf("%d\n", one_appear_count_by_binary(test)); printf("%d\n", one_appear_count_by_binary(32+1)); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/4 numer/string_to_integer.c ================================================ #include "stdio.h" #include "string.h" int string_to_integer(char *s,int length){ if (length>1) { return s[0]=='-'?string_to_integer(s,length-1)*10-(s[length-1]-'0'):string_to_integer(s,length-1)*10+s[length-1]-'0'; }else{ return s[0]=='-'?-1/10:s[0]-'0'; } } /* 问题: 1. s是空字符串 2. s传参为非数字字符 3. 超出整数所能表示范围 */ int main(){ char *s = "4323"; printf("integer == %d\n",string_to_integer(s,strlen(s)) ); printf("integer == %d\n",string_to_integer("12342",5)); printf("integer == %d\n",string_to_integer("-11",3) ); printf("integer == %d\n",string_to_integer("-5",2) ); printf("integer == %d\n", string_to_integer("3",1)); printf("integer == %d\n", string_to_integer("abcd",4)); printf("integer == %d\n", string_to_integer("a",1)); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/4-1.c ================================================ #include "stdio.h" int main(){ printf("%ld",236432123443*33453098); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/5 array/delete_occurence_character.c ================================================ #include "stdio.h" char *delete_occurence_character(char *src , char target){ char *front = src; char *rear = src; while(*front != '\0'){ if (*front != target){ *rear = *front; rear++; } front++; } *rear = '\0'; return src; } int main(int argc, char const *argv[]) { char test[] = "abcdeccba"; printf("%s\n", delete_occurence_character(test,'c')); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/5 array/factorial.c ================================================ #include "stdio.h" int factorial(int n){ int ret=0; (n==0) || (ret=n+factorial(n-1)); return ret; } int main(){ int n=10; printf("factorial(10)= %d\n",factorial(10)); printf("factorial(5)= %d\n",factorial(5)); printf("factorial(1)= %d\n",factorial(1)); printf("factorial(0)= %d\n",factorial(0)); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/5 array/fibonacci.c ================================================ #include "stdio.h" #include "time.h" int fibonacci(int n){ int result[2] = {0,1}; if (n<2) { return result[n]; } return fibonacci(n-1)+fibonacci(n-2); } // 循环 int fibonacci2(int n){ int result[2] = {0,1}; if ( n<2 ) { return result[n]; } int fibOne=0; int fibTwo=1; int fibN; for (int i = 2; i < n; ++i) { fibN = fibOne + fibTwo; fibOne = fibTwo; fibTwo = fibN; } return fibN; } int main(){ int start,end; long result; start = clock(); result=fibonacci2(40); end = clock(); printf("%ld %.6fs",result,(double)(end-start)/CLOCKS_PER_SEC); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/5 array/longest_continuious_number.c ================================================ #include "stdio.h" int longest_continuious_number(const char *input,char *output){ int max = 0; char *start= input; char *mid = input; char *end = input; int tmp = 0; if (input == NULL || output == NULL) return 0; while (*end != '\0'){ if (*end < '0' || *end > '9'){//字母 if(tmp > max){ max = tmp; start = mid; } tmp = 0; }else{//数字 if (tmp == 0){//发现数字 mid = end; } tmp++; } end++; } //修改已数字结尾的bug if(tmp > max){ max = tmp; start = mid; } //copy int i=0; while(in){ sum-=small; small++; if (sum==n) { printf("%d, %d\n",small,big); } } big++; sum+=big; } } int main(int argc, char const *argv[]) { print_continuous_sequence_sum(115); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/5.c ================================================ #include "stdio.h" /* 1 是第一个元素 序列中元素被2或3或5整除 */ unsigned long value_in_sequence(unsigned index){ } int main(){ return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/6 matrix/print_matrix.c ================================================ #include "stdio.h" // a[i*rows+j] void print_matrix_incircle(int *matrix,int rows,int cols,int start){ int endX = cols-start-1; int endY = rows-start-1; //然后依次打印 上,右,下,左 for(int i=start;i<=endX;i++){ printf("%d ",matrix[start*rows+i]); } for(int i=start+1;i<=endY;i++){ printf("%d ",matrix[i*rows+endX]); } for(int i=endX-1;i>=start;i--){ printf("%d ",matrix[endY*rows+i]); } for(int i=endY-1;i>start;i--){ printf("%d ",matrix[i*rows+start]); } } void print_matrix(int *matrix,int rows,int cols){ if(matrix==NULL) return ; if(rows<=0 || cols<=0) return; int start = 0; while(start*2=key)//往左找 { high = mid; }else{ low = mid+1; } } return high; } int binary_search_last(int *a,int length,int key){ int low = 0; int high = length -1; int mid = 0; while(low #define TOTAL_ELEMENTS (sizeof(array) / sizeof(array[0])) int array[] = {23,34,12,17,204,99,16}; int main() { int d; int n = TOTAL_ELEMENTS-2; for(d=-1;d <= (TOTAL_ELEMENTS-2);d++) printf("%d\n",array[d+1]); return 0; } /* d 是有符号数,sizeof()计算为无符号数,比较时d会转为无符号数,变成一个大数 */ ================================================ FILE: 9 Algorithms Job Interview/codes/c10.c ================================================ #include int main() { int i=43; printf("%d\n",printf("%d",printf("%d",i))); return 0; } // printf 返回值是打印数据的长度 4321 ================================================ FILE: 9 Algorithms Job Interview/codes/c11-2.c ================================================ #include void foobar1(void) { printf("In foobar1\n"); } void foobar2() { printf("In foobar2\n"); } int main() { char ch = 'a'; foobar1(); foobar2(); return 0; } // compile error ================================================ FILE: 9 Algorithms Job Interview/codes/c11.c ================================================ #include void foobar1(void) { printf("In foobar1\n"); } void foobar2() { printf("In foobar2\n"); } int main() { char ch = 'a'; foobar1(); foobar2(33, ch); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/c12.c ================================================ // FROM:http://www.gowrikumar.com/c/ #include int main() { float a = 12.5; printf("%d\n", a); //printf("%d\n", *(int *)&a); return 0; } // 浮点数 转 整形 ,用强制转换(int)a /* 浮点数的存储 */ ================================================ FILE: 9 Algorithms Job Interview/codes/c13-1.c ================================================ int arr[80]; ================================================ FILE: 9 Algorithms Job Interview/codes/c13-2.c ================================================ #include "c13-1.c" int main() { return 0; } /** 类型并不匹配,所以导致这里的arr并没有指向c13-1.c中声明的arr[80],应该修改为 extern int arr[] int *arr 和 int arr[80] 区别? */ ================================================ FILE: 9 Algorithms Job Interview/codes/c14.c ================================================ #include int main() { int a=1; switch(a) { int b=20; case 1: printf("b is %d\n",b); break; default:printf("b is %d\n",b); break; } return 0; } // In my computer ,output:b is 32767 switch中只会从case开始执行,b会随机输出 ================================================ FILE: 9 Algorithms Job Interview/codes/c15.c ================================================ #include #define SIZE 10 void size(int arr[SIZE]) { printf("size of array is:%d\n",sizeof(arr)); } int main() { int arr[SIZE]; size(arr); return 0; } // 32位机器 4, 64位机器 8 ================================================ FILE: 9 Algorithms Job Interview/codes/c16.c ================================================ // FROM:http://www.gowrikumar.com/c/ #include #include void Error(char* s) { printf(s); return; } int main() { int *p; p = malloc(sizeof(int)); if(p == NULL) { Error("Could not allocate the memory\n"); Error("Quitting....\n"); exit(1); } else { /*some stuff to use p*/ Error("Could not allocate the memory\n"); Error("Quitting....\n"); Error(5); } return 0; } /* 潜在的问题是: Error函数中,如果输入的参数不是字符串,比如传一个整数5,5被转成一个内存地址,printf这时候访问就会出问题了。 $1 = 0x5 */ ================================================ FILE: 9 Algorithms Job Interview/codes/c17.c ================================================ // FROM:http://www.gowrikumar.com/c/ #include int main() { char c; scanf("%c",&c); printf("%c\n",c); scanf(" %c",&c); printf("%c\n",c); return 0; } /* scanf("%c",&c) 和 scanf(" %c",&c) 区别 使用第二个scanf("%c",&c) 时,系统会将前一个scanf()输入的回车符号读入改变量。 这里为什么加一个“空格”就可以? scanf带“空格”后,会从输入缓冲区中skip 空白符(空格、tab,换行符),读取一个字符 */ ================================================ FILE: 9 Algorithms Job Interview/codes/c18.c ================================================ #include int main() { int i; i = 10; printf("i : %d\n",i); printf("sizeof(i++) is: %d\n",sizeof(i++)); printf("i : %d\n",i); return 0; } // sizeof()不会对传入的表达式计算 ,所以后面的输入还是i:10 ================================================ FILE: 9 Algorithms Job Interview/codes/c19.c ================================================ #include #include #define SIZEOF(arr) (sizeof(arr)/sizeof(arr[0])) #define PrintInt(expr) printf("%s:%d\n",#expr,(expr)) int main() { /* The powers of 10 */ int pot[] = { 0001, 0010, 0100, 1000 }; int i; for(i=0;i void OS_Solaris_print() { printf("Solaris - Sun Microsystems\n"); } void OS_Windows_print() { printf("Windows - Microsoft\n"); } void OS_HP_UX_print() { printf("HP-UX - Hewlett Packard\n"); } int main() { int num; printf("Enter the number (1-3):\n"); scanf("%d",&num); switch(num) { case 1: OS_Solaris_print(); break; case 2: OS_Windows_print(); break; case 3: OS_HP_UX_print(); break; default: printf("Hmm! only 1-3 :-)\n"); break; } return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/c20.c ================================================ int main() { char ch1; char ch2; ch1 = getchar(); ch2 = getchar(); printf("%d %d", ch1, ch2); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/c3.c ================================================ #include enum {false,true}; int main() { int i=1; do { printf("%d\n",i); i++; if(i < 15) continue; }while(false); return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/c4.c ================================================ #include #include int main() { while(1) { fprintf(stdout,"hello-out"); fprintf(stderr,"hello-err"); sleep(1); } return 0; } /* stdout 会缓冲输出 \n fflush() setbuf(stdout,NULL) */ ================================================ FILE: 9 Algorithms Job Interview/codes/c5.c ================================================ // FROM:http://www.gowrikumar.com/c/ #include #define f(a,b) a##b #define g(a) #a #define h(a) g(a) int main() { printf("%s\n",h(f(1,2))); // printf("%s\n",g(f(1,2))); // f(1,2) return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/c6.c ================================================ #include int main() { int a=10; switch(a) { case '1': printf("ONE\n"); break; case '2': printf("TWO\n"); break; a: printf("NONE\n"); } return 0; } //什么也不输出, “default”这里随便什么都没有编译错误 ================================================ FILE: 9 Algorithms Job Interview/codes/c7.c ================================================ #include int main() { int* p; p = (int*)malloc(sizeof(int)); *p = 10; return 0; } ================================================ FILE: 9 Algorithms Job Interview/codes/c8.c ================================================ #include int main() { float f=0.0f; int i; for(i=0;i<10;i++) f = f + 0.1f; if(f == 1.0f) printf("f is 1.0 \n"); else printf("f is NOT 1.0\n"); return 0; } // 浮点数判相等 ================================================ FILE: 9 Algorithms Job Interview/codes/c9.c ================================================ #include int main() { int a = (1,2); printf("a : %d\n",a); return 0; } // ,运算符优先级 最低 ,括号不能少 // ================================================ FILE: 9 Algorithms Job Interview/codes/most_visit_ip.c ================================================ #include "stdio.h" // fwrite #include "unistd.h" // write #include "stdlib.h" // itoa #include "string.h" #define test_file_path2 "./ip2.txt" #define test_file_path "./ip.txt" #define ip_count 100000000 //随机1亿个IP #define tmp_file_count 32 #define mem_count 128*1024*1028 //128MB个IP空间 int hash(unsigned i){ return i>>27; } int main(){ // 模拟创建一个文件,产生测试用的IP // FILE *fd; // fd = fopen(tmp_file_path,"a+"); // for (int i = 0; i < ip_count;++i) // { // //char c[] = "this is sentence!"; // unsigned random_ip = (unsigned)rand(); // char tmp[50]; // sprintf(tmp,"%d",random_ip); // //printf("random ip = %s\n",tmp); // fwrite(tmp,strlen(tmp),1,fd); // fputc('\n',fd);//加入换行符号 // } //创建临时文件 FILE *fd[tmp_file_count]; for (int i = 0; i < tmp_file_count; ++i) { char file_path[128]; //s[0]=(char)(i+48); //char *file_path = strcat("./ipslice/ipslice",s) ; // 这样写出现 bus error!!! sprintf(file_path,"./ipslice/ipslice%d",i); fd[i] = fopen(file_path ,"a+" ); if ( !fd[i]) { printf("file %d open fail!! \n" ,i); } } //开始读测试数据IP,按IP,映射到32个文件中 FILE *testfd = fopen(test_file_path2,"r"); if (!testfd) { printf("file open fail !!\n"); exit(-1); } unsigned tmp_data; while( getline() ){ //能读到数据 // fread(&tmp_data,sizeof(unsigned),1,testfd) , C语言中没有getline() 这样的函数 printf("%d\n",(unsigned)tmp_data ); int key = hash((unsigned)tmp_data);// key值即为对应文件fd int size = fwrite(&tmp_data,sizeof(unsigned),1,fd[key]); printf("写入数据size=%d ,key=%d\n",size, key ); //跳过换行符 fseek(testfd,1,SEEK_CUR); } // 依次读入每个文件并统计, hash_map 统计每个区间段的最大IP int hash_map[mem_count]; int max_ip; int max_times; for (int i = 0; i < tmp_file_count; ++i) { // hash统计 while(){} //hash 表里找出最大的,变量一遍 for (int i = 0; i < mem_count; ++i) { /* code */ } } // 释放 for (int i = 0; i < tmp_file_count; ++i) { fclose(fd[i]); } return 0; } /** 关于warning implicit declaration of function 'itoa' is invalid in C99 itoa() 函数并不是一个 standard functions ANSI C standard inet_ntop() // 整数转换成 .分 ip地址 字符串 inet_nton() // inet_aton() // struct in_addr */ ================================================ FILE: 9 Algorithms Job Interview/剑指offer/README.md ================================================ # 《剑指offer》 《剑指offer》 这本书给出了50到面试题,涉及到字符串处理,堆栈,链表,二叉树等问题的处理。 * 代码鲁榜性:边界条件,特殊输入,异常处理:空指针 * 分析方法:画图,举例,分解 * 查找和排序是常考:重点掌握二分查找,快速排序,归并排序 * 本书完整源代码在: # 数值 ### 二进制中1的个数 输入一个整数,输出该数二进制中1出现的次数。比如9的二进制 10001,输出是2 `n=n&n-1` ``` int one_appear_count(int n) ``` ### 数值的整数次方 要求不得使用库函数。这里注意考虑指数是0和负数的情况 ``` double power(double base,int exponent) ``` ### 打印1到最大的n位数 比如n=3,就打印1到999 ``` void print_to_max_with_length(int n) ``` ### 求 1+2+...+n 要求不用乘除法,for/while/if/else/switch等关键字及条件判断语句 ``` long long sum(unsigned int n); ``` ### 不用加减乘除做加法 求2个整数之和 `位运算` ``` int sum(int a,int b ``` ### 丑数 > 只包含因子 2,3,5的数叫做丑数;比如 6(2x3), 8(2x2x2) 是丑数(ugly number) 求按从小到大的顺序,第1500个丑数 ``` int ugly(int n) ``` # 字符串 ### 替换空格 如把字符串中的每个空格替换成`%20` `二遍扫描` ``` void replace_blank(char *str); ``` ### 把字符串转换成整数 比如 "12343567754" -> 12343567754 `NULL,空串,正负号,溢出` ``` int strToInt(char str); ``` ### 第一个只出现一次的字符 在字符串中查找第一个只出现一次的字符 `哈希表:值为出现的次数` `二次扫描` ``` char find_appear_once_char(char *string) ``` ### 字符串的排列 输入一个字符串,打印该字符串中字符的所有排列 `递归,分解` ``` void print_full_permutation(char *string) ``` ### 反转单词顺序 VS 左旋转字符串 a. 翻转句子中单词的顺序,但单词内字符不变。如 『I am a student』 -> 『student. a am I』 `先以单词为单位翻转,整个句子再次翻转` ``` char *reverse_by_word(char *string) ``` b. 左旋转字符串是把字符串其那面的若干位转义到字符串的尾部。比如"abcedfsz"和数字2,结果是"cedfszab" ``` char *left_rotate_string(char *s,int n) ``` # 链表 ### 从尾到头打印链表 `栈` ``` void print_reversing(LinkList *head) ``` ### 两个链表的第一个公共节点 `长的链表先走k步` ``` LinkListNode *common_node(LinkList *head1,LinkList head2); ``` ### 在O(1)时间删除链表节点 已经有一个头节点指针,还有一个指向改删除节点的指针 `用下一个节点的内容覆盖当前删除节点的内容,删除下一个节点` ``` void deleteNode(LinkList *head,LinkList *targetToDelete); ``` ### 输出链表中倒数第K个节点 `使用两个指针,一个先走k-1步` ``` void print_lastK(LinkList *head); ``` ### 反转链表 `三个指针` ``` void reverse(LinkList *head); ``` 反转二叉树呢? ### 合并2个排序的链表 要求合并以后链表任然排序 `递归` ``` LinkList *merge(LinkList *one,LinkList *two); ``` ### 复杂链表的复制 在复杂链表结构中,每个节点都有一个指向下一个节点的m_pNext;还有一个指向任意节点的m_pSibling ``` typedef struct LinkListNode{ int m_value; LinkListNode *m_pNext; LinkListNode *m_pSlbling; }LinkList; ``` ``` LinkList * copy(LinkList *head); ``` # 列表&数列 ### 数组中出现次数超过一半的数字 `遍历数组,下一个数字和之前保存的数字一样就+1,否则就-1` ``` int find_more_than_half_num(int *nums ,int length) ``` ### n个整数中最小的K个数 `快速排序` `最大堆` ``` void find_least_k(int *data,int n,int *ouput,int k) ``` 最大的K个数呢? ### 连续子数组的最大和 输入一个整数数组,有正有负,求所有子数组的和的最大值 `分析规律` `动态规划` ``` int max_of_subarray(int *data,int length) ``` ### 从1到n整数中,1出现的次数 比如 12,从1到12这些整数中,包含1的数字有 1, 10,11,12 ,1出现了5次 ``` int one_appear_count(int n) ``` ### 把数组排成最小的数 输入一个正整数数组,把所有数字拼起来排出一个最小数 ``` int minSort(int *nums, int length); ``` ### 菲波那切数列 > 波那切数列 fabonacci , 也叫黄金分割数列, F (0)=0, F (1)=1, F (n)= F (n - 1)+ F (n - 2) ; 即 0、1、1、2、3、5、8、13、21、34、…… ``` long long fabonacci(unsigned n) ``` ### 调整数组顺序使奇数位于偶数前面 调整后,所有奇数在前半部分,偶数在后半部分 `两边向中间扫描` ``` void reorder(int *data,int length) ``` ### 旋转数组的最小数字 旋转数组是指把一个数组最开始的若干个元素搬到数组的末尾。输入一个递增排序的数组的旋转,比如{3,4,5,1,2}是{1,2,3,4,5}的一个旋转。求该数组的最小值。 ``` int min(int *num, int length) ``` ### 数组中只出现一次的数字 数组中除了2个数字之外,其他的数组都出现了2次,找出这两个数 `异或` `二进制` >如果是只有1个数字只出现一次,我们可以通过对数组依次做异或运算。 如果我们能把原数组分成2个子数组,每个子数组都包含一个只出现一次的数字,问题就能解决了。我们把数组中的所有数字依次做异或操作,如果有2个数字不一样,结果肯定不是0,且异或结果数字的二进制表示中至少有一位是1(不然结果不就是0了) 1. 在结果数字二进制表示中找到第一个为1的位的位置,标记n 2. 以二进制表示中第n位是不是1为标准,把原数组分成2个子数组 ``` void find_two_numbers_appear_once(int *data,int length,int *ouput) ``` ### 和为s的两个数字 VS 和为s的连续正数序列 有一个递增排序数组,和一个数字s,找出数组中的2个数,使得和等于s。输出任意一对即可 `两边向中间扫描` ``` void print_two_numbers(int *data,int length,int sum) ``` ### 数组中的逆序对 > 数组中的两个数字如果前面一个数字大于后面的数字,这两个数字组成一个逆序对。如:[7,5,6,4] 的逆序对:(7,5)(7,6)(7,4)(5,4)(6,4) 输入一个数组,求出这个数组逆序对总数。 `归并排序 O(nlogn),空间O(n)` ``` int reversePairs(int *data,int length) ``` ### 数字在排序数组中出现的次数 比如 {1,2,3,3,3,3,4,5}, 数字 3出现了4次 `使用二分查找找第一个3,和最后一个3出现的位置` ``` int appear_count(int *nums,int length,int n); ``` ### n个色子的点数 把n个色子丢地上,朝上一面的点数之和为s。输入n,打印可能的值出现的概率 ``` void print_sum_probability(int n) ``` ### 扑克牌中的顺子 从扑克牌从随机抽5张牌,判断是不是顺子。A是1,J~K是11~13,大小王可以看出任意数字。 ``` bool is_straight(int *data,int length) ``` ### 圆圈中最后剩下的数字(约瑟夫问题) > 约瑟夫问题: 又称为约瑟夫环, N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3 0,1,...,n-1 这n个数字排成一个圆圈,从数字0开始从这个圆圈里面删除第m个数字,求出这个圆圈里最后剩下的数字。 ``` int last_remaining(unsigned int n,unsigned int m) ``` # 栈 & 队列 ### 用两个栈实现队列 队列就是在尾部插入节点,头部删除节点。 ### 实现一个能找到栈的最小元素的函数 `最小元素用辅助栈保存` ``` int min(Stack *stack) ``` ### 栈的压入,弹出序列 输入2个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。比如: 1,2,3,4,5是压栈序列,4,5,3,2,1是弹栈序列,但是4,3,5,1,2就不是弹栈序列 ``` bool is_pop_order(int *push,int *pop,int length) ``` # 矩阵 > 矩阵用二维数组表示 ### 从外向里顺时针打印矩阵 ``` void print_matrix_clockwise(int *matrix,int cols,int rows); ``` 延伸:按大小顺序打印矩阵 ### 二维数组中的查找 二维数组中每一行从左到右递增,每一列从上到下递增,判断数组中是否包含该整数。 ``` bool find(int *matrix,int rows,int columns,int numbers) ``` # 二叉树 ### 重建二叉树 输入某二叉树的前序遍历和中序遍历的结果,重建该二叉树 ``` BinaryTree *construct(int *preorder,int inroder,int length); ``` ### 树的子结构 考察二叉树的基本操作。输入2课二叉树A和B,判断B是不是A的子结构。 ``` struct BinaryTreeNode{ int m_value; BinaryTreeNode *m_pleft; BinaryTreeNode *m_pRight; } ``` ``` 8 / \ 10 / \ / \ 6 10 子结构 11 9 / \ / \ 5 7 9 11 ``` ``` bool subTree(BinaryTreeNode *root1,BinaryTreeNode *root2); ``` ### 二叉树翻转 ``` 8 8 / \ / \ / \ / \ 6 10 翻转后 10 6 / \ / \ / \ / \ 5 7 9 11 11 9 7 5 ``` `交换每个节点的左右子树` ``` void reverse(BinaryTreeNode *root); ``` ### 从上往下打印二叉树 `辅助队列` ``` void print_binary_level(BinaryTreeNode *root) ``` ### 二叉搜索树的后续遍历序列 输入一个整数数组,判断该数组是不是某二叉查找树的后续遍历序列的结果。比如【 5,7,6,9,11,10,8】 是下面二叉查找树的后续遍历结果: ``` 8 / \ / \ 6 10 / \ / \ 5 7 9 11 ``` `寻找规律` ``` bool is_post_order(BST *root,int *data, int length); ``` ### 二叉树中和为某一值的路径 ``` 10 / \ / \ 5 12 / \ 5 7 ``` 和为22的路径有2条:10--5--7, 10--12 `递归,栈` ``` void print_path(BinaryTree *root,int n) ``` ### 二叉搜索树与双向链表 将二叉搜索树转换成一个排序的双向链表,只调整树中节点的指针指向 `递归` `分解问题` ``` BST *transform(BST *root); ``` ### 二叉树的深度 `递归` ``` int tree_depth(BTree *root); ``` ### 树中2个结点的最低公共祖先 如果这个树是二叉排序树 如果不是二叉排序树,但是有父节点指针 如果不是二叉树,也没有父节点指针 # 其他 ### 不能被继承的类 ``` ``` ### 实现Singleton模式 ### 赋值运算符函数 ================================================ FILE: 9 Algorithms Job Interview/编程之美/README.md ================================================ #《编程之美》 书中的内容分为4个部分: 1. 游戏之乐:游戏中的一些问题 2. 数字之魅: 数字和字符的处理能力 3. 结构之法: 对字符串,链表,队列,树的操作 4. 数学之趣: 一些数学问题 《剑指offer》中已经出现的题目先不写了 # 游戏之乐 # 数字之魅 #### 二进制数中1的个数 #### 阶乘 #### 寻找发帖"水王" #### 1的数目 #### 寻找最大的k个数 #### 精确表达浮点数 #### 最大公约数 #### 找符合条件的整数 #### Fibonacci数列 #### 寻找数组中的最大值和最小指 #### 寻找最近点对 #### 快速寻找满足条件的两个数 #### 子数组的最大乘积 #### 求数组的子数组之和的最大值 #### 子数组之和的最大值 #### 数组中最长递归子序列 #### 数组循环移位 #### 数组分割 #### 区间重合判断 # 结构之法 #### 字符串移位包含的问题 #### 电话号码应对英语单词 #### 计算字符串的相似度 #### 从无头单链表中删除节点 #### 最短摘要生成 #### 判断2个链表是否相交 #### 队列中取最大值 #### 求二叉树中节点的最大距离 #### 重建二叉树 #### 程序改错 # 数学之趣 #### ================================================ FILE: 91 Algorithms In Big Data/Bitmap.md ================================================ # Bitmap 也就是用1个(或几个)bit位来标记某个元素对应的value(如果是1bitmap,就只能是元素是否存在;如果是x-bitmap,还可以是元素出现的次数等信息)。使用bit位来存储信息,在需要的存储空间方面可以大大节省。应用场景有: 1. 判断某个元素是否存在 2. 排序(如果是1-bitmap,就只能对无重复的数排序) 比如,某文件中有若干8位数字的电话号码,要求统计一共有多少个不同的电话号码? 分析:8位最多99 999 999, 如果1Byte表示1个号码是否存在,需要95MB空间,但是如果1bit表示1个号码是否存在,则只需要 95/8=12MB 的空间。这时,数字 `k(0~99 999 999)`与bit位的对应关系是: ``` #define SIZE 15*1024*1024 char a[SIZE]; memset(a,0,SIZE); // a[k/8]这个字节中的 `k%8` 位命中,置为1 // 这里要注意 big-endian 和 little-endian的问题 ,假设这里是big-endian a[k/8] = a[k/8] | (0x01 << (k%8)) ``` ================================================ FILE: 91 Algorithms In Big Data/Bloomfilter.md ================================================ # Bloom filter(布隆过滤器) Bloom Filter是由Bloom在1970年提出的一种多哈希函数映射的快速查找算法。通常应用在海量数据处理中,一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合(容忍错误的场景)。 ## Bloom filter 特点 为了说明Bloom Filter存在的重要意义,举一个实例:假设要你写一个网络蜘蛛(web crawler)。由于网络间的链接错综复杂,蜘蛛在网络间爬行很可能会形成“环”。为了避免形成“环”,就需要知道蜘蛛已经访问过那些URL。给一个URL,怎样知道蜘蛛是否已经访问过呢?稍微想想,就会有如下几种方案: 1. 将访问过的URL保存到数据库。 2. 用HashSet将访问过的URL保存起来。那只需接近O(1)的代价就可以查到一个URL是否被访问过了。 3. URL经过MD5或SHA-1等单向哈希后再保存到HashSet或数据库。 4. BitMap方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。 方法1~3都是将访问过的URL完整保存,方法4则只标记URL的一个映射位。以上方法在数据量较小的情况下都能完美解决问题,但是当数据量变得非常庞大时问题就来了。 方法1的缺点:数据量变得非常庞大后关系型数据库查询的效率会变得很低。而且每来一个URL就启动一次数据库查询是不是太小题大做了? 方法2的缺点:太消耗内存。随着URL的增多,占用的内存会越来越多。就算只有1亿个URL,每个URL只算50个字符,就需要5GB内存。 方法3:由于字符串经过MD5处理后的信息摘要长度只有128Bit,SHA-1处理后也只有160Bit,因此方法3比方法2节省了好几倍的内存。 方法4消耗内存是相对较少的,但缺点是单一哈希函数发生冲突的概率太高。还记得数据结构课上学过的Hash表冲突的各种解决方法么?若要降低冲突发生的概率到1%,就要将BitSet的长度设置为URL个数的100倍。 实质上上面的算法都忽略了一个重要的隐含条件:允许小概率的出错,不一定要100%准确!也就是说少量url实际上没有没网络蜘蛛访问,而将它们错判为已访问的代价是很小的——大不了少抓几个网页呗。 ## 应用场景 * 爬虫对URL 去重,避免爬重复的URL; 爬虫Url重复(去重):如果几个亿几十亿的url装在一个集合上 ,比较浪费空间,把url映射到布隆过滤器,纯新的url一定会爬取, 少部分(比如0.01%)被判断为重复url可能也是新url ,会缺掉一些网页而已。 * 垃圾邮箱问题,垃圾邮箱地址映射到bloomfilter,如果是垃圾邮箱,一定会被抓,这个能保证。无非是一些好人也被抓,这个可以通过给这些可伶的被误伤的设置个白名单就OK * 避免缓存穿透,使用BloomFilter把所有数据放到bit数组中,当用户请求时存在的值肯定能放行,部分不存在的值也会被放行,绝大部分会被拦截。在DSP广告系统中,使用bloomfilter减少对Redis缓存读取,通过设备id读取用户信息( key为设备id,value hashmap 用户信息) ; 由于大量设备id都不是滴滴用户 (80%以上),redis中都没有用户信息,因此会产生大量无效redis读取。使用BF可以减轻Redis读取压力。 * 减少磁盘IO: Google bigtable, Apache HBase 使用 BloomFilter 防止不必要的磁盘IO * 减少网络请求:相同请求拦截,防止被攻击,也就是去重 Redis 4.0 中通过 布隆过滤器插件 来支持,redis 布隆过滤器主要就两个命令: * bf.add 添加元素到布隆过滤器中:bf.add urls https://jaychen.cc。 * bf.exists 判断某个元素是否在过滤器中:bf.exists urls https://jaychen.cc。 ### 比特币 SPV钱包 比特币 SPV(simple payment verification)钱包应用中 使用 BloomFilter 加速钱包同步。 spv 主要用于移动支付的场景,不可能下载所有全节点数据,几百G 在2012年 BIP37之前,SPV的做法是将所有的区块和交易都下载下来,然后本地将不相关的交易给删掉。当然带来的问题就是同步慢、浪费带宽、增加内存使用。在BIP-37中就提到了因为这一点,导致用户对手机APP“Bitcoin Wallet”有所抱怨。 钱包余额多少? * 保护隐私,SPV节点不用告诉相邻全节点自己的所有钱包地址,而只是说一个 可能存在于bloomfilter里的钱包地址集合 * 通过bloomfilter 过滤出 utxo,可能属于钱包地址,不在bloomfilter中地址对应的utxo一定会被过滤 ## Bloom filter 算法 Bloom filter bitmap 数组 和 k个hash函数。 使用 bitmap 位图数据结构,本质是一个bit位数组, 用 一个 bit 位标记对应Value的 取值(0 or 1); 判断一个值是否存在,就是看对应的bitmap位是否为 1 布隆过滤器还需要有 k 个哈希函数,进行如下操作: * 使用 K 个哈希函数对元素值进行 K 次计算,得到 K 个哈希值。 * 根据得到的哈希值,在位数组中把对应下标的值置为 1。 比如有个url https://jaychen.cc , 有 3 个哈希函数:f1, f2, f3 和一个位数组 arr , 现在要把 url 插入布隆过滤器中: * 对值进行三次哈希计算,得到三个值 n1, n2, n3。 * 把位数组中三个元素 arr[n1], arr[n2], arr[3] 置为 1。 现在需求是:要判断一个 url 是否在, 布隆过滤器中,对元素再次进行哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值很大可能在布隆过滤器中;如果存在一个值不为 1,说明该元素肯定不在布隆过滤器中。 Scrapy-Redis的去重机制中,一个url指纹存储未 40长度的16 进制数,如27adcc2e8979cdee0c9cecbbe8bf8ff51edefb61 占用 20Byte内存空间,1亿个指纹占用2 GB Bloom filter可以看做是对bitmap的扩展。只是使用多个hash映射函数,从而减低hash发生冲突的概率。算法如下: 1. 创建 m 位的bitset,初始化为0, 选中k个不同的哈希函数 2. 第 i 个hash 函数对字符串str 哈希的结果记为 h(i,str) ,范围是(0,m-1) 3. 将字符串记录到bitset的过程:对于一个字符串str,分别记录h(1,str),h(2,str)...,h(k,str)。 然后将bitset的h(1,str),h(2,str)...,h(k,str)位置1。也就是将一个str映射到bitset的 k 个二进制位。 4. 检查字符串是否存在:对于字符串str,分别计算h(1,str)、h(2,str),...,h(k,str)。然后检查BitSet的第h(1,str)、h(2,str),...,h(k,str) 位是否为1,若其中任何一位不为1则可以判定str一定没有被记录过。若全部位都是1,则“认为”字符串str存在。但是若一个字符串对应的Bit全为1,实际上是不能100%的肯定该字符串被Bloom Filter记录过的。(因为有可能该字符串的所有位都刚好是被其他字符串所对应)这种将该字符串划分错的情况,称为false positive 。 5. 删除字符串:字符串加入了就被不能删除了,因为删除会影响到其他字符串。实在需要删除字符串的可以使用Counting bloomfilter(CBF)。 `Bloom Filter 使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。` ### 最优的哈希函数个数,位数组m大小 哈希函数的选择对性能的影响应该是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。 在原始个数位n时,那这里的k应该取多少呢?位数组m大小应该取多少呢?这里有个计算公式:`k=(ln2)*(m/n)`, 当满足这个条件时,错误率最小。 假设错误率为0.01, 此时m 大概是 n 的13倍,k大概是8个。 这里的n是元素记录的个数,m是bit位个数。如果每个元素的长度原大于13,使用Bloom Filter就可以节省内存。 ### 错误率估计 ## 实现示例 ``` #define SIZE 15*1024*1024 char a[SIZE]; /* 15MB*8 = 120M bit空间 */ memset(a,0,SIZE); int seeds[] = { 5, 7, 11, 13, 31, 37, 61}; int hashcode(int cap,int seed, string key){ int hash = 0; for (int i=0;i>> 16)) & Integer.MAX_VALUE; } } ``` ``` public class ByteBufferBloomFilter { /** * 存储BloomFilter数据 */ private final ByteBuffer data; private final int size;//占用空间 /** * 构造BloomFilter * @param size 占用空间(字节数),应设为key总数的1.5倍以上,最大不超过2G */ public ByteBufferBloomFilter(int size) { if (size <= 0) { throw new IllegalArgumentException("size must > 0"); } this.size = size; this.data = ByteBuffer.allocateDirect(size); } @Override public void put(String key) { int[] hs = Hash.hashes(key, size); for (int i = 0; i < hs.length; i++) { int idx = hs[i]; int b = data.get(idx); data.put(idx, (byte) (b | (1 << i))); } } @Override public boolean contains(String key) { int[] hs = Hash.hashes(key, size); for (int i = 0; i < hs.length; i++) { int b = data.get(hs[i]); if ((b & (1 << i)) == 0) { return false; } } return true; } @Override public int size() { return size; } } ``` 对每个字符串str求哈希就可以使用 `hashcode(SIZE*8,seeds[i],str)` ,i 的取值范围就是 (0,k)。 ## 参考 http://www.cnblogs.com/heaad/archive/2011/01/02/1924195.html http://blog.csdn.net/jiaomeng/article/details/1495500 http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html `哈希函数个数k、位数组大小m` 测试论证 https://blog.csdn.net/tianyaleixiaowu/article/details/74721877 https://juejin.im/post/5bc7446e5188255c791b3360 ================================================ FILE: 91 Algorithms In Big Data/Hash映射,分而治之.md ================================================ # Hash映射,分而治之 这里的`Hash映射`是指通过一种映射散列的方式,将海量数据均匀分布在对应的内存或更小的文件中 使用hash映射有个最重要的特点是: `hash值相同的两个串不一定一样,但是两个一样的字符串hash值一定相等`。哈希函数如下: ``` int hash = 0; for (int i=0;i=2) ,有一些特征: 1. 根节点至少有2个子节点 2. 所有的叶节点具有相同的深度 h,也就是树高 3. 每个叶子节点至少包含一个key和2个指针,最多2d-1个key和2d个指针,叶节点的指针都是null。每个节点的关键字个数在【d-1,2d-1】之间 4. 每个非叶子节点,key和指针互相间隔,节点两端是指针,因此节点中指针个数=key的个数+1 5. 每个指针要么是null,要么指向另一个节点 如果某个指针在节点node最左边且不为null,则其指向节点的所有key小于v(key1),其中v(key1)为node的第一个key的值。 如果某个指针在节点node最右边且不为null,则其指向节点的所有key大于v(keym),其中v(keym)为node的最后一个key的值。 如果某个指针在节点node的左右相邻key分别是keyi和keyi+1且不为null,则其指向节点的所有key小于v(keyi+1)且大于v(keyi)。 使用数据结构表示如下: ``` typedef struct Item{ int key; Data data; } #define m 3 //B树的阶 typedef struct BTNode{ int degree; //B树的度 int keynums; //每个节点key的个数 Item items[m]; struct BTNode *p[m]; }BTNode,* BTree; typedef struct{ BTNode *pt; //指向找到的节点 int i; // 节点中关键字的序号 (0,m-1) int tag; //1:查找成功,0:查找失败 }Result; Status btree_insert(root,target); Status btree_delete(root,target); Result btree_find(root,target); ``` ### 建立索引 当为一张空表创建索引时,数据库系统将为你分配一个索引页,该索引页在你插入数据前一直是空的。此页此时既是根结点,也是叶结点。每当你往表中插入一行数据,数据库系统即向此根结点中插入一行索引记录。 插入和删除新的数据记录都会破坏B-Tree的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持B-Tree性质 ### 查找操作 从root节点出发,对每个节点,找到等于target的key,则查找成功;或者找到大于target的最小k[i], 到 k[i] 左指针指向的子节点继续查找,直到页节点,如果找不到,说明关键字target不在B树中。 分析下时间复杂度: 对于一个度为d的B-Tree,每个节点的索引key个数是d-1, 索引key个数为N,树高h上限是: 2d^h-1=N ==> h=logd^((N+1) /2) ??? 因此,检索一个key,查找节点的个数的复杂度是O(logd^N) ``` 比如d=2,N=1,000,000 (1百万),h差不多20个 d=3,N=1,000,000 (1百万) ,h差不多13个(3^11=1,594,323) d=4,N=1,000,000 (1百万) ,h差不多10个 d=5,N=1,000,000 (1百万) ,h差不多9个 (5^9 = 1,953,125) d=6,N=1,000,000 (1百万) ,h差不多8个(6^8 = 1,679,616) d=7,N=1,000,000 (1百万) ,h差不多8个 d=8,N=1,000,000 (1百万) ,h差不多7个 d=9,N=1,000,000 (1百万) ,h差不多7个 d=10,N=1,000,000 (1百万) ,h差不多6个 d=100时,h差不多3个 ``` 数据库系统在设计时,通常将一个节点的大小设为一个页大小(通常4k),这样保证一个节点在物理上也存储在一个页里,加上计算机存储分配都是按页对其,这样保证一个节点只需要一次I/O. 实际应用中,d都是比较大,通常超过100,因此1百万的数据通常最多访问3个节点,也就是3次I/O, 因此使用B树作为索引结构查询效率非常高。 ### 插入数据 插入数据时,需要更新索引,索引中也要添加一条记录。索引中添加一条记录的过程是: 沿着搜索的路径从root一直到叶节点 每个节点的关键字个数在【d-1,2d-1】之间,当节点的关键字个数是2t-1时,再加入target就违反了B树定义,需要对该节点进行分裂:已中间节点为界,分成2个包含 `d-1` 个关键字的子节点(另外还有一个分界关键字,`2*(d-1)+1=2d-1)`,同时把该分界关键字提升到该叶子的父节点中,如果这导致父节点关键字个数超过 `2d-1`, 就继续向上分裂,直到根节点。 如下演示动画,往度d=2的B树中插入:` 6 10 4 14 5 11 15 3 2 12 1 7 8 8 6 3 6 21 5 15 15 6 32 23 45 65 7 8 6 5 4` ![](./btree_insert.gif) ### B树和B+树的区别 B树和B+树的区别在于: 1. B+树的非叶子节点只包含导航信息,不包含实际记录的信息,这可以保证一个固定大小节点可以放入更多个关键字,也就是更大的度d,从而树高h可以更小,从而相比B树有更优秀的查询效率 2. 所有的叶子节点和相邻的节点使用链表方式相连,便于区间查找和遍历 ================================================ FILE: 91 Algorithms In Big Data/README.md ================================================ # 海量数据处理 所谓海量数据,就是数据量太大,要么在短时间内无法计算出结果,要么数据太大,无法一次性装入内存。 针对时间,我们可以使用巧妙的算法搭配合适的数据结构,如bitmap/堆/trie树等 针对空间,就一个办法,大而化小,分而治之。常采用hash映射 * [Hash映射,分而治之](Hash映射,分而治之.md) * [Bitmap](Bitmap.md) * [Bloom filter(布隆过滤器)](Bloomfilter.md) * [双层桶划分](双层桶划分.md) * [Trie树](../4%20Tree/4-字典树Trie/README.md) * [数据库索引](Inverted%20Index/数据库索引.md) * 倒排索引(Inverted Index) * [外排序](../6%20Sort/外排序.md) * [simhash算法](simhash算法.md) * 分布处理之Mapreduce ### 估算 在处理海量问题之前,我们往往要先估算下数据量,能否一次性载入内存?如果不能,应该用什么方式拆分成小块以后映射进内存?每次拆分的大小多少合适?以及在不同方案下,大概需要的内存空间和计算时间。 比如,我们来了解下以下常见问题`时间` 和 `空间` 估算 : ``` 8位的电话号码,最多有99 999 999个 IP地址 1G内存,2^32 ,差不多40亿,40亿Byte*8 = 320亿 bit ``` 海量处理问题常用的分析解决问题的思路是: * 分而治之/Hash映射 + hash统计/trie树/红黑树/二叉搜索树 + 堆排序/快速排序/归并排序 * 双层桶划分 * Bloom filter 、Bitmap * Trie树/数据库/倒排索引 * 外排序 * 分布处理之 Hadoop/Mapreduce ================================================ FILE: 91 Algorithms In Big Data/mapreduce/Hash映射,分而治之.md ================================================ # Hash映射,分而治之 这里的`Hash映射`是指通过一种映射散列的方式,将海量数据均匀分布在对应的内存或更小的文件中 使用hash映射有个最重要的特点是: `hash值相同的两个串不一定一样,但是两个一样的字符串hash值一定相等`。哈希函数如下: ``` int hash = 0; for (int i=0;i>27 结果就是[0,31],把相同区间的IP保存到同一个文件 上面的映射中,`都不会出现同一个IP映射到不同小文件的情况` #### 统计每个小文件中的最大值 现在文件不同的IP数量达到内存承受的范围了,可以统计小文件中的IP次数了,使用常规的 hash_map(IP,count) 来 统计了。可以分块读取来减少磁盘IO #### 排序 堆排序/快速排序/归并排序 #### 代码示例 ``` ``` #### 类似问题 1. 怎么在海量数据中找出重复次数最多的一个? 2. 服务器内存1G,有一个2G的文件,里面每行存着一个QQ号(5-10位数),怎么最快找出出现过最多次的QQ号。(10位数可以表示(不到)10亿个QQ号) #### top-K 找出最大的k个数 类似的题目类型有: 100w个数中找出最大的100个数。 寻找热门查询,300万个查询字符串中统计最热门的10个查询。 有一个文件中保存了2亿个整数,每个整数都以' '分隔。求最大的100万个整数之和。 有一千万条短信,有重复,以文本文件的形式保存,一行一条,有重复。请用5分钟时间,找出重复出现最多的前10条。 一个文件中包含了1亿个随机整数,如何快速的找到最大(小)的100万个数字?(时间复杂度:O(n lg k)) 有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。 分析: hashmap统计+二叉堆 方案1:在前面的题中,我们已经提到了,用一个含100个元素的最小堆完成。复杂度为O(100w*lg100)。 方案2:采用快速排序的思想,每次分割之后只考虑比轴大的一部分,知道比轴大的一部分在比100多的时候,采用传统排序算法排序,取前100个。复杂度为O(100w*100)。 方 案3:采用局部淘汰法。选取前100个元素,并排序,记为序列L。然后一次扫描剩余的元素x,与排好序的100个元素中最小的元素比,如果比这个最小的要 大,那么把这个最小的元素删除,并把x利用插入排序的思想,插入到序列L中。依次循环,知道扫描了所有的元素。复杂度为O(100w*100)。 这里给个最简单hash函数,`字符串到int类型的哈希` ``` int hash_function(const char *p) { int value = 0; while (*p != '\0') { value = value * 31 + *p++; if (value > HASHLEN) value = value % HASHLEN; } return value; } ``` 这个hash函数要确保 不同的字符串 hash出不同的一个 整数。 类似的题目还有: ##### 有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。 方案1:顺序读文件中,对于每个词x,取clip_image014,然后按照该值存到5000个小文件(记为clip_image016) 中。这样每个文件大概是200k左右。如果其中的有的文件超过了1M大小,还可以按照类似的方法继续往下分,知道分解得到的小文件的大小都不超过1M。对 每个小文件,统计每个文件中出现的词以及相应的频率(可以采用trie树/hash_map等),并取出出现频率最大的100个词(可以用含100个结点 的最小堆),并把100词及相应的频率存入文件,这样又得到了5000个文件。下一步就是把这5000个文件进行归并(类似与归并排序)的过程了。 ##### 一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。 方 案1:这题是考虑时间效率。用trie树统计每个词出现的次数,时间复杂度是O(n*le)(le表示单词的平准长度)。然后是找出出现最频繁的前10个 词,可以用堆来实现,前面的题中已经讲到了,时间复杂度是O(n*lg10)。所以总的时间复杂度,是O(n*le)与O(n*lg10)中较大的哪一 个。 ##### 一个文本文件,找出前10个经常出现的词,但这次文件比较长,说是上亿行或十亿行,总之无法一次读入内存,问最优解。 方案1:首先根据用hash并求模,将文件分解为多个小文件,对于单个文件利用上题的方法求出每个文件件中10个最常出现的词。然后再进行归并处理,找出最终的10个最常出现的词。 ##### 寻找热门查询: 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较 高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询 串,要求使用的内存不能超过1G。 (1) 请描述你解决这个问题的思路; (2) 请给出主要的处理流程,算法,以及算法的复杂度。 方案1:采用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。 #### 分布式中的top-K 海量数据分布在100台电脑中,想个办法高效统计出这批数据的TOP10。 方案1: s 在每台电脑上求出TOP10,可以采用包含10个元素的堆完成(TOP10小,用最大堆,TOP10大,用最小堆)。比如求TOP10大,我们首先取前 10个元素调整成最小堆,如果发现,然后扫描后面的数据,并与堆顶元素比较,如果比堆顶元素大,那么用该元素替换堆顶,然后再调整为最小堆。最后堆中的元 素就是TOP10大。 s 求出每台电脑上的TOP10后,然后把这100台电脑上的TOP10组合起来,共1000个数据,再利用上面类似的方法求出TOP10就可以了。 #### 统计指定时间段内ip访问量 假设某个网站每天有超过10亿次的页面访问量,出于安全考虑,网站会记录访问客户端访问的ip地址和对应的时间,如果现在已经记录了1000亿条数据,想统计一个指定时间段内的区域ip地址访问量,那么这些数据应该按照何种方式来组织,才能尽快满足上面的统计需求呢, 设计完方案后,并指出该方案的优缺点,比如在什么情况下,可能会非常慢? 参考答案:用B+树来组织,非叶子节点存储(某个时间点,页面访问量),叶子节点是访问的IP地址。这个方案的优点是查询某个时间段内的IP访问量很快,但是要统计某个IP的访问次数或是上次访问时间就不得不遍历整个树的叶子节点。或者可以建立二级索引,分别是时间和地点来建立索引。 #### 文件中的内容排序并去重复 1) 如果文件太大,就分片 2) 这里的问题是去除重复,分片后,小文件合并又是个麻烦事。 3) 直接hash统计,假设不重复的url最多300万,采用hash存储需要的空间 300万*4 linux中的几个命令: ``` sort 将文件内容排序 uniq 检查和删除文件内容中重复出现的行列 comm 比较2个已经排序的文件 ``` 对于较大文件 G级别的文件 split -b 把大文件分割成小文件 然后用 sort 分别排序 sort -m 合并结果 然后在 uniq 命令去重 类似的题目有: 大量的URL字符串,如何从中去除重复的,优化时间空间复杂度 对2亿条手机号码删除重复记录 #### 在O(n lg k)时间内,将k个排序表合并成一个排序表,n为所有有序表中元素个数 【解析】取前100 万个整数,构造成了一棵数组方式存储的具有小顶堆,然后接着依次取下一个整数,如果它大于最小元素亦即堆顶元素,则将其赋予堆顶元素,然后用Heapify调整整个堆,如此下去,则最后留在堆中的100万个整数即为所求 100万个数字。该方法可大大节约内存。 #### 搜索中的智能提示(如 "中国" ,"zhongguo",考虑拼音) 把2万个汉字排序,弄成一个超长的字符串 用Int16索引汉字的所有拼音 Int64 建立汉字和拼音的关联--汉字有多音字,需要把多个拼音pack到一个Int64,位操作搞定 二分+位移unpack,直接做到汉字到拼音的检索 #### 找出文件中相反的串对 一个文件,内含一千万行字符串,每个字符串在1K以内,要求找出所有相反的串对,如abc和cba。 分析: 文件大小上限是:1000万*1K ~ 10GB。不可能内存操作 1) 设计hash函数 使得相反串得到相同的hash值 ? #### 求出这个文件里的整数里不包含的一个整数 一个文件中有40亿个整数,每个整数为四个字节,内存为1GB,写出一个算法:求出这个文件里的整数里不包含的一个整数 40亿*4 = 15GB, 4个字节表示的整数,总共有2^32个可能 使用`位图法`来统计: 1)分配 2^29 X 2^3 = 512MB 的内存空间,每一个bit代表一个整数,全部初始化为0; buffer[512x1024x1024] 2) 读入一个数,把对应的bit位置为1. 如读入的是 312 , 将对应的bit置为1:312/8=39 312%8=0 ,写入位置是 第40个字节,0号bit 3) 处理完40亿数据后,遍历500M内存,找到一个bit为0的位 for(unsigned int i=0;i<0xffffffff,i++){ if(!buffer){//为0则不包括这个整数 } } `位图法` 适用于大规模数据,通常用来判断某个数据存不存在 写入指定位: bytepos = i/8 = i>>3 bitpos = i%8 = i & 0x1F 置为1:arr[bytepos] |= (1 << bitpos) 置为0:arr[bytepos] &= !(1<< bitpos) void setbit(int *bitmap,int i){ bitmap[i>>3] |= (1 << (i&0x1F) ); } 读指定位: getbit(int *bitmap,int i){ return bitmap[i>>3] & ( 1 << (i&0x1F) ); } #### 对文件里的整数排序 假设一个文件中有9亿条不重复的9位整数,现在要求对这个文件进行排序。 9位整数做多可表示10亿条不重复整数(无符号数) 9亿*4=3.4GB内存,一次读入内存是不可能了。 思路一: 位图法排序 1) 计算需要内存: 9亿/8/1024/1024 < 120MB ,全部初始化为0 2) 读取文件中的数据,将数据对应的bit位置1 `分段读取` 3) 遍历整个bit,将bit为1的依次存入文件 思路二:? #### {url,size} 查询子串并按size排序 给出一个文件,里面包含两个字段{url、size},即url为网址,size为对应网址访问的次数; 要求: 问题1、利用Linux Shell命令或自己设计算法, 查询出url字符串中包含“baidu”子字符串对应的size字段值; 问题2、根据问题1的查询结果,对其按照size由大到小的排列。 (说明:url数据量很大,100亿级以上) #### 找出文件中的中位数 在一个文件中有 10G 个整数,乱序排列,要求找出中位数。内存限制为 2G。只写出思路即可。 #### 找出重复登陆的QQ号 腾讯服务器每秒有2w个QQ号同时上线,找出5min内重新登入的qq号并打印出来。 #### 海量节点树中寻找共同祖先 有一个一亿节点的树,现在已知两个点,找这两个点的共同的祖先。 #### 设计流量统计系统 某服务器流量统计器,每天有1000亿的访问记录数据,包括时间、url、ip。设计系统实现记录数据的保存、管理、查询。要求能实现一下功能: (1)计算在某一时间段(精确到分)时间内的,某url的所有访问量。 (2)计算在某一时间段(精确到分)时间内的,某ip的所有访问量。 #### a,b文件中共同的记录 1. 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url? 方案1:可以估计每个文件安的大小为50G×64=320G,远远大于内存限制的4G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。 s 遍历文件a,对每个url求取clip_image002,然后根据所取得的值将url分别存储到1000个小文件(记为clip_image004)中。这样每个小文件的大约为300M。 s 遍历文件b,采取和a相同的方式将url分别存储到1000各小文件(记为clip_image006)。这样处理后,所有可能相同的url都在对应的小文件(clip_image008)中,不对应的小文件不可能有相同的url。然后我们只要求出1000对小文件中相同的url即可。 s 求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。 方 案2:如果允许有一定的错误率,可以使用Bloom filter,4G内存大概可以表示340亿bit。将其中一个文件中的url使用Bloom filter映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)。 2. 有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。 方案1: s 顺序读取10个文件,按照hash(query)%10的结果将query写入到另外10个文件(记为clip_image010)中。这样新生成的文件每个的大小大约也1G(假设hash函数是随机的)。 s 找一台内存在2G左右的机器,依次对clip_image010[1]用hash_map(query, query_count)来统计每个query出现的次数。利用快速/堆/归并排序按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件(记为clip_image012)。 s 对clip_image012[1]这10个文件进行归并排序(内排序与外排序相结合)。 方案2: 一般query的总量是有限的,只是重复的次数比较多而已,可能对于所有的query,一次性就可以加入到内存了。这样,我们就可以采用trie树/hash_map等直接来统计每个query出现的次数,然后按出现次数做快速/堆/归并排序就可以了。 方案3: 与方案1类似,但在做完hash,分成多个文件后,可以交给多个文件来处理,采用分布式的架构来处理(比如MapReduce),最后再进行合并。 #### 找出不重复的整数(QQ号) 在2.5亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数。 方案1:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存clip_image020内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是01的整数输出即可。 方案2:也可采用上题类似的方法,进行划分小文件的方法。然后在小文件中找出不重复的整数,并排序。然后再进行归并,注意去除重复的元素。 #### 字符串去重 1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现? 方案1:这题用trie树比较合适,hash_map也应该能行。 #### 如何随机选取1000个关键字 给定一个数据流,其中包含无穷尽的搜索关键字(比如,人们在谷歌搜索时不断输入的关键字)。如何才能从这个无穷尽的流中随机的选取1000个关键字? 类似的题目还有: 有一个很大很大的输入流,大到没有存储器可以将其存储下来,而且只输入一次,如何从这个输入流中随机取得m个记录 #### 最大间隙问题 给定n个实数clip_image042,求着n个实数在实轴上向量2个数之间的最大差值,要求线性的时间算法。 方案1:最先想到的方法就是先对这n个数据进行排序,然后一遍扫描即可确定相邻的最大间隙。但该方法不能满足线性时间的要求。故采取如下方法: s 找到n个数据中最大和最小数据max和min。 s 用n-2个点等分区间[min, max],即将[min, max]等分为n-1个区间(前闭后开区间),将这些区间看作桶,编号为clip_image044,且桶clip_image046的上界和桶i+1的下届相同,即每个桶的大小相同。每个桶的大小为:clip_image048。实际上,这些桶的边界构成了一个等差数列(首项为min,公差为clip_image050),且认为将min放入第一个桶,将max放入第n-1个桶。 s 将n个数放入n-1个桶中:将每个元素clip_image052分配到某个桶(编号为index),其中clip_image054,并求出分到每个桶的最大最小数据。 s 最大间隙:除最大最小数据max和min以外的n-2个数据放入n-1个桶中,由抽屉原理可知至少有一个桶是空的,又因为每个桶的大小相同,所以最大间隙 不会在同一桶中出现,一定是某个桶的上界和气候某个桶的下界之间隙,且该量筒之间的桶(即便好在该连个便好之间的桶)一定是空桶。也就是说,最大间隙在桶 i的上界和桶j的下界之间产生clip_image056。一遍扫描即可完成。 #### 将多个集合合并成没有交集的集合:给定一个字符串的集合,格式如:clip_image058。要求将其中交集不为空的集合合并,要求合并完成的集合之间无交集,例如上例应输出clip_image060。 (1) 请描述你解决这个问题的思路; (2) 给出主要的处理流程,算法,以及算法的复杂度; (3) 请描述可能的改进。 方案1:采用并查集。首先所有的字符串都在单独的并查集中。然后依扫描每个集合,顺序合并将两个相邻元素合并。例如,对于clip_image062, 首先查看aaa和bbb是否在同一个并查集中,如果不在,那么把它们所在的并查集合并,然后再看bbb和ccc是否在同一个并查集中,如果不在,那么也把 它们所在的并查集合并。接下来再扫描其他的集合,当所有的集合都扫描完了,并查集代表的集合便是所求。复杂度应该是O(NlgN)的。改进的话,首先可以 记录每个节点的根结点,改进查询。合并的时候,可以把大的和小的进行合,这样也减少复杂度。 #### 最大子序列与最大子矩阵问题 数组的最大子序列问题:给定一个数组,其中元素有正,也有负,找出其中一个连续子序列,使和最大。 方案1:这个问题可以动态规划的思想解决。设clip_image064表示以第i个元素clip_image066结尾的最大子序列,那么显然clip_image068。基于这一点可以很快用代码实现。 最大子矩阵问题:给定一个矩阵(二维数组),其中数据有大有小,请找一个子矩阵,使得子矩阵的和最大,并输出这个和。 方案1:可以采用与最大子序列类似的思想来解决。如果我们确定了选择第i列和第j列之间的元素,那么在这个范围内,其实就是一个最大子序列问题。如何确定第i列和第j列可以词用暴搜的方法进行。代码详见我的博客。 #### 一共有N个机器,每个机器上有N个数。每个机器最多存O(N)个数并对它们操作。如何找到clip_image022个数中的中数? 方案1:先大体估计一下这些数的范围,比如这里假设这些数都是32位无符号整数(共有clip_image018[1]个)。我们把0到clip_image024的整数划分为N个范围段,每个段包含clip_image026个整数。比如,第一个段位0到clip_image028,第二段为clip_image026[1]到clip_image030,…,第N个段为clip_image032到clip_image024[1]。 然后,扫描每个机器上的N个数,把属于第一个区段的数放到第一个机器上,属于第二个区段的数放到第二个机器上,…,属于第N个区段的数放到第N个机器上。 注意这个过程每个机器上存储的数应该是O(N)的。下面我们依次统计每个机器上数的个数,一次累加,直到找到第k个机器,在该机器上累加的数大于或等于clip_image034,而在第k-1个机器上的累加数小于clip_image034[1],并把这个数记为x。那么我们要找的中位数在第k个机器中,排在第clip_image036位。然后我们对第k个机器的数排序,并找出第clip_image036[1]个数,即为所求的中位数。复杂度是clip_image038的。 方案2:先对每台机器上的数进行排序。排好序后,我们采用归并排序的思想,将这N个机器上的数归并起来得到最终的排序。找到第clip_image034[2]个便是所求。复杂度是clip_image040的。 ================================================ FILE: 92 Algorithms In DB/README.md ================================================ # 数据库系统中的算法 最近开始读《数据库系统实现》这本书,所以就想到把数据库里面用到的数据结构和算法做一个梳理。就有了这些文字。 * 电梯算法 * B树索引 * R树索引 * 位图索引 * 一趟算法 * 二趟算法 * 基于排序 * 基于散列 * 连接树 * 动态规划 * 贪婪算法 * 分布式并行数据库中的任务分配算法 * 并行算法 * 数据挖掘 * 发现频繁项集的算法 * 发现近似商品的算法 * PageRank ## 参考 《数据库系统实现》 《redis设计与实现》 ================================================ FILE: 92 Algorithms In DB/mysql/README.md ================================================ ================================================ FILE: 92 Algorithms In DB/redis/README.md ================================================ # ================================================ FILE: 93 Algorithms In Open Source/Bitcoin/Merkle Tree.md ================================================ # Merkle Tree ================================================ FILE: 93 Algorithms In Open Source/GeoHash/多维空间点索引算法.md ================================================ # 多维空间点索引算法 经纬度信息与GeoHash值互转 redis 中也有 geohash 算法的实现。 ================================================ FILE: 93 Algorithms In Open Source/README.md ================================================ ## 开源项目中用到的算法 数据结构往往是一个项目系统的核心,理解项目的数据结构和算法,才能真正理解项目的工作原理。这里聊聊`开源项目`中用到的数据结构和算法。 针对每个项目,我们试图说清楚: 1. 项目简介 2. 用到的算法介绍 ### 开源项目 * [YYCache](https://github.com/ibireme/YYCache.git) * [cocos2d-objc](https://github.com/cocos2d/cocos2d-objc) * [AsyncDisplayKit](https://github.com/facebook/AsyncDisplayKit) * [realm-cocoa](https://github.com/realm/realm-cocoa) * [YapDatabase](https://github.com/yapstudios/YapDatabase) * [FTCoreText](https://github.com/Ridiculous-Innovations/FTCoreText) * [Redis](https://github.com/antirez/redis) * [memcached](https://github.com/memcached/memcached) * [Nginx](https://github.com/nginx/nginx) * [Node](https://github.com/nodejs/node) V8引擎 * [React](https://github.com/facebook/react) * [react-native](https://github.com/facebook/react-native) * ... ================================================ FILE: 93 Algorithms In Open Source/Timer/timer.md ================================================ # timer * 小顶堆/最小堆 * 红黑树 * 时间轮 ================================================ FILE: 93 Algorithms In Open Source/YYKit/YYCache.md ================================================ # YYCache [YYCache](https://github.com/ibireme/YYCache.git) 是 iOS 系统上一套线程安全的 `Key-Value` 缓存实现,使用 `Objective-C` 语言实现。`YYCache` 缓存使用 `双向链表队列+hash表结构` 实现。 ================================================ FILE: 93 Algorithms In Open Source/kafka/README.md ================================================ # ================================================ FILE: 93 Algorithms In Open Source/nginx/README.md ================================================ # ================================================ FILE: 93 Algorithms In Open Source/zoomkeeper/README.md ================================================ # ================================================ FILE: 94 15-Classic-Algorithms/README.md ================================================ # 15个经典基础算法 * A*寻路算法 :求解最短路径 * Dijkstra:最短路径算法 * 动态规划 (Dynamic Programming) * BFS/DFS (广度/深度优先遍历) * 红黑树 一种自平衡的`二叉查找树` * KMP 字符串匹配算法 * 遗传算法 * 启发式搜索 * 图像特征提取之SIFT算法 * 傅立叶变换 * Hash * 快速排序 * SPFA(shortest path faster algorithm) 单元最短路径算法 * 快递选择SELECT ================================================ FILE: C Language Code Specification.md ================================================ ## 补充:C语言代码规范 这里面的算法代码均使用C语言完成,养成良好的代码规范习惯,不但可以写出优质的代码,也可以更快的阅读其他优秀开源代码。代码规范主要有: ### 符号命名 **局部变量** 尽量短,能表达清楚意思即可,能简写就简写,比如"err" 表示 "error"; "fd" 表示文件描述符 ,循环变量可以使用i,j,k ;结构体成员变量不需要"m_"前缀;全局变量"g_"开头 **常量名** 全大写,单词之间"_"分割,如 "MAX_NUMBER_OF_SLAB_CLASSES" ; **宏定义** 对于options 宏定义,适当使用前缀 ,比如: ``` /* Client classes for client limits, currently used only for * the max-client-output-buffer limit implementation. */ #define CLIENT_TYPE_NORMAL 0 /* Normal req-reply clients + MONITORs */ #define CLIENT_TYPE_SLAVE 1 /* Slaves. */ #define CLIENT_TYPE_PUBSUB 2 /* Clients subscribed to PubSub channels. */ #define CLIENT_TYPE_MASTER 3 /* Master. */ #define CLIENT_TYPE_OBUF_COUNT 3 ``` **枚举** 使用前缀: ``` enum conn_states { conn_listening, /**< the socket which listens for connections */ conn_new_cmd, /**< Prepare connection for next command */ conn_waiting, /**< waiting for a readable socket */ conn_read, /**< reading in a command line */ conn_parse_cmd, /**< try to parse a command from the input buffer */ conn_write, /**< writing out a simple response */ conn_nread, /**< reading in a fixed number of bytes */ conn_swallow, /**< swallowing unnecessary bytes w/o storing */ conn_closing, /**< closing this connection */ conn_mwrite, /**< writing out many items sequentially */ conn_closed, /**< connection is closed */ conn_max_state /**< Max state value (used for assertion) */ }; ``` **函数命名** 全小写,单词之间"_"分割。如"split_cmdline_strerror()" ### 注释 所有注释使用 "/* 这里是注释 */ " ### 其他 合理使用static,const 等关键字,能提升程序的安全性,也能避免函数命名冲突 合理使用数据类型:rel_time_t,uint8_t,uint32_t,uint64_t,size_t,off_t ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ #The file is in Chinese >算法虐我千百遍,我待算法如初恋 这里的内容是我学习算法过程的一些记录,希望能一直坚持下去。 ## 学习方法 * 把所有经典算法写一遍 * 看算法有关源码 * 加入算法学习社区,相互鼓励学习 * 看经典书籍 * 刷题 ## 基本数据结构和算法 这些算法全部自己敲一遍: ### [链表](2%20List/README.md) * 链表 * 双向链表 ### [数组](2%20List/数组.md) * [数组数列问题](9%20Algorithms%20Job%20Interview/5%20数组数列问题.md) 数组和链表结构是基础结构,散列表、栈、队列、堆、树、图等等各种数据结构都基于数组和链表结构实现。 ### [队列](2%20Queue/README.md) * 队列 * 堆栈 ### [哈希表 HashTable](3%20Hash%20Table/README.md) * 散列函数 * 碰撞解决 ### [字符串算法](1%20String/README.md) * 子串查找 [字符串常见题目参考这里](9%20Algorithms%20Job%20Interview/1%20字符串.md) * BF算法 * KMP算法 * BM算法 * 正则表达式 * 数据压缩 * 排序 ### [树](4%20Tree/README.md) * 二叉树 [快速排序](6%20Sort/README.md)就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历 * [二叉查找树BST](4%20Tree/2-二叉查找树/二叉查找树.md) 有序的二叉树,中序遍历结果是递增的 * [平衡二叉树 AVL树](4%20Tree/3-平衡树AVL/README.md) 绝对平衡二叉树; * [红黑树](4%20Tree/9-红黑树%20R-B%20tree/红黑树.md) 弱平衡二叉树;使用广泛 * [B树](4%20Tree/7-B树/B树.md) * [B+树](4%20Tree/7-B树/B+树.md) mysql 索引使用 B+树 的数据结构 * [字典树trie](4%20Tree/4-字典树Trie/README.md) 字典树也叫前缀树,单词查找树 * [二叉堆](4%20Tree/8-堆/堆.md) * [伸展树](4%20Tree/5-伸展树/伸展树.md) * [后缀树](4%20Tree/6-后缀树/后缀树.md) * 斐波那契堆(Fibonacci Heap) * 最优二叉树(赫夫曼树) ### [图的算法](5%20Graph/README.md) * 图的存储结构和基本操作(建立,遍历,删除节点,添加节点) * 最小生成树 * 拓扑排序 * 关键路径 * 最短路径: Floyd,Dijkstra,bellman-ford,spfa ### [排序算法](6%20Sort/README.md) **交换排序算法** * 冒泡排序 * 插入排序 * 选择排序 * 希尔排序 * 快排 * 归并排序 * 堆排序 **线性排序算法** * 桶排序 ### [查找算法](7%20Search/README.md) * 哈希表: O(1) [hashtable实现参考这里](../3%20Hash%20Table/README.md) * 有序表查找:二分查找 * 顺序表查找:顺序查找, 复杂度O(N) * 分块查找: 块内无序,块之间有序;可以先二分查找定位到块,然后再到`块`中顺序查找 * 动态查找: 二叉排序树,AVL树,B- ,B+(这里之所以叫 `动态查找表`,是因为表结构是查找的过程中动态生成的) ## [算法设计思想](8%20Algorithms%20Analysis/README.md) * [递归](8%20Algorithms%20Analysis/递归.md) * [分治算法](8%20Algorithms%20Analysis/分治算法.md) * [动态规划](8%20Algorithms%20Analysis/动态规划.md) * [回溯法](8%20Algorithms%20Analysis/回溯法.md) * [迭代法](8%20Algorithms%20Analysis/迭代法.md) * [穷举搜索法](8%20Algorithms%20Analysis/穷举搜索法.md) * [贪心算法](8%20Algorithms%20Analysis/贪心算法.md) ## [面试算法题目](9%20Algorithms%20Job%20Interview/README.md) 这是一个算法题目合集,题目是我从网络和书籍之中整理而来,部分题目已经做了思路整理。问题分类包括: * 字符串 * 堆和栈 * 链表 * 数值问题 * 数组和数列问题 * 矩阵问题 * 二叉树 * 图 * 海量数据处理 * 智力思维训练 * 系统设计 还有部分来自算法网站和书籍: * 九度OJ * leetcode * 剑指offer ## [海量数据处理](91%20Algorithms%20In%20Big%20Data/README.md) * Hash映射/分而治之 * Bitmap * Bloom filter(布隆过滤器) * Trie树 * 数据库索引 * 倒排索引(Inverted Index) * 双层桶划分 * 外排序 * simhash算法 * 分布处理之Mapreduce ## [开源项目中的算法](93%20Algorithms%20In%20Open%20Source/README.md) * YYCache * cocos2d-objc * bitcoin * geohash * kafka * nginx * zookeeper * ... ## 15个经典基础算法 * [KMP 字符串匹配算法](1%20String/KMP.md) * [Hash](3%20Hash%20Table/README.md) * [快速排序](6%20Sort/README.md) * 快速选择SELECT * [红黑树 (一种弱/自平衡的`二叉查找树`)](4%20Tree/9-红黑树%20R-B%20tree/红黑树.md) * [BFS/DFS (广度/深度优先遍历)](5%20Graph/DFS%20和%20BFS.md) * [`A*`寻路算法: 求解最短路径](5%20Graph/最短路径.md) * Dijkstra:最短路径算法 * `SPFA(Shortest Path Faster Algorithm)` 单元最短路径算法 * 启发式搜索 * 遗传算法 `GA` * [DP (动态规划 dynamic programming)](8%20Algorithms%20Analysis/动态规划.md) * 图像特征提取之`SIFT` 算法 , 广泛的应用于图像识别,图像检索,3D重建等CV的各种领域 * 傅立叶变换 ## 推荐阅读 ### 刷题必备 * 《剑指offer》 * 《编程之美》 * 《编程之法:面试和算法心得》   * 《算法谜题》 都是思维题 ### 基础 * 《编程珠玑》Programming Pearls * 《编程珠玑(续)》 * 《数据结构与算法分析》 * 《Algorithms》 这本近千页的书只有6章,其中四章分别是排序,查找,图,字符串,足见介绍细致 ### 算法设计 * 《算法设计与分析基础》 * 《算法引论》 告诉你如何创造算法 断货 * 《Algorithm Design Manual》算法设计手册 红皮书 * 《算法导论》 是一本对算法介绍比较全面的经典书籍 * 《Algorithms on Strings,Trees and Sequences》 * 《Advanced Data Structures》 各种诡异高级的数据结构和算法 如元胞自动机、斐波纳契堆、线段树 600块 ### 延伸阅读 * 《深入理解计算机系统》 * 《TCP/IP详解三卷》 * 《UNIX网络编程二卷》 * 《UNIX环境高级编程:第2版》 * 《The practice of programming》 Brian Kernighan和Rob Pike * 《writing efficient programs》 优化 * 《The science of programming》 证明代码段的正确性 800块一本 ## 参考链接和学习网站 ### [July 博客](http://blog.csdn.net/v_july_v) * 《数学建模十大经典算法》 * 《数据挖掘领域十大经典算法》 * 《十道海量数据处理面试题》 * 《数字图像处理领域的二十四个经典算法》 * 《精选微软等公司经典的算法面试100题》 * [The-Art-Of-Programming-By-July](https://github.com/julycoding/The-Art-Of-Programming-By-July) * [微软面试100题](http://blog.csdn.net/column/details/ms100.html) * [程序员编程艺术](http://blog.csdn.net/v_JULY_v/article/details/6460494) ### 基本算法演示 http://sjjg.js.zwu.edu.cn/SFXX/sf1/sfys.html http://www.cs.usfca.edu/~galles/visualization/Algorithms.html ### 编程网站 * [leetcode](http://leetcode.com/) * [codetop](https://codetop.cc/home) 企业高频面试题库,刷题必备 * [openjudge](http://openjudge.cn/) 开放在线程序评测平台,可以创建自己的OJ小组   * [九度OJ](http://ac.jobdu.com/index.php) * 这有个[ACM训练方案](http://www.java3z.com/cwbwebhome/article/article19/res041.html) ### 网课 [高级数据结构和算法](https://www.coursera.org/learn/gaoji-shuju-jiegou/) 北大教授张铭老师在coursera上的课程。完成这门课之时,你将掌握多维数组、广义表、Trie树、AVL树、伸展树等高级数据结构,并结合内排序、外排序、检索、索引有关的算法,高效地解决现实生活中一些比较复杂的应用问题。当然coursera上也还有很多其它算法方面的视频课程。 [算法设计与分析 Design and Analysis of Algorithms](https://class.coursera.org/algorithms-001/lecture) 由北大教授Wanling Qu在coursera讲授的一门算法课程。首先介绍一些与算法有关的基础知识,然后阐述经典的算法设计思想和分析技术,主要涉及的算法设计技术是:分治策略、动态规划、贪心法、回溯与分支限界等。每个视频都配有相应的讲义(pdf文件)以便阅读和复习。 ### 其它 [OI Wiki](https://github.com/24OI/OI-wiki/) 主要内容是 OI/ACM-ICPC 编程竞赛 (competitive programming) 相关的知识整理, 包括基础知识、常见题型、解题思路以及常用工具等内容。 [labuladong 的算法小抄](https://labuladong.gitee.io/algo/) 作者整理了很多的解题套路框架,看完获益良多 ## 联系 [@移动开发小冉](http://weibo.com/ranwj)