数据结构大全(二)

112 篇文章 5 订阅
70 篇文章 4 订阅

Manacher

小问题一:请问,子串和子序列一样么?请思考一下再往下看

小问题二:长度为n的字符串有多少个子串?多少个子序列?

一、分析枚举的效率

二、初步优化

问题三:怎么用对称轴向两边扩的方法找到偶回文?(容易操作的)

那么请问,加进去的符号,有什么要求么?是不是必须在原字符中没出现过?请思考

小结:

三、Manacher原理

假设遍历到位置i,如何操作呢

四、代码及复杂度分析

前缀树

后缀树/后缀数组

后缀树:后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。

 

相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等。

后缀数组:就是把某个字符串的所有后缀按照字典序排序后的数组。(数组中保存起始位置就好了,结束位置一定是最后)

AC自动机

数组缺失

二叉树遍历

前序

中序

后序

进一步思考

二叉树序列化/反序列化

先序中序后序两两结合重建二叉树

先序遍历

中序遍历

后序遍历

层次遍历

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

输入某二叉树的后序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

输入某二叉树的后序遍历和先序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

先序中序数组推后序数组

二叉树遍历

遍历命名

方法1:我们可以重建整棵树:

https://blog.csdn.net/hebtu666/article/details/84322113

方法2:我们可以不用重建,直接得出:

根据数组建立平衡二叉搜索树

java整体打印二叉树

判断平衡二叉树

判断完全二叉树

判断二叉搜索树

二叉搜索树实现

堆的简单实现

堆应用例题三连

一个数据流中,随时可以取得中位数。

金条

项目最大收益(贪心问题)

 并查集实现

并查集入门三连:HDU1213 POJ1611 POJ2236

HDU1213

POJ1611

 POJ2236

线段树简单实现

功能:一样的,依旧是查询和改值。

查询[s,t]之间最小的数。修改某个值。

那我们继续说,如何查询。

如何更新?

 树状数组实现

最大搜索子树

morris遍历

最小生成树

拓扑排序

最短路

 

简单迷宫问题

深搜DFS\广搜BFS 

 皇后问题

一般思路:

优化1:

优化2:

二叉搜索树实现

Abstract Self-Balancing Binary Search Tree

 

二叉搜索树

概念引入

AVL树

红黑树

size balance tree

伸展树

Treap

最简单的旋转

带子树旋转

代码实现

AVL Tree

前言

二叉搜索树

AVL Tree

旋转

旋转总结

单向右旋平衡处理LL:

单向左旋平衡处理RR:

双向旋转(先左后右)平衡处理LR:

双向旋转(先右后左)平衡处理RL:

深度的记录

单个节点的深度更新

写出旋转代码

总写调整方法

插入完工

删除

直观表现程序

跳表介绍和实现

c语言实现排序和查找所有算法

 

 

 

 

 

Manacher

Manacher's Algorithm 马拉车算法操作及原理 

 
  1. package advanced_001;

  2.  
  3. public class Code_Manacher {

  4.  
  5. public static char[] manacherString(String str) {

  6. char[] charArr = str.toCharArray();

  7. char[] res = new char[str.length() * 2 + 1];

  8. int index = 0;

  9. for (int i = 0; i != res.length; i++) {

  10. res[i] = (i & 1) == 0 ? '#' : charArr[index++];

  11. }

  12. return res;

  13. }

  14.  
  15. public static int maxLcpsLength(String str) {

  16. if (str == null || str.length() == 0) {

  17. return 0;

  18. }

  19. char[] charArr = manacherString(str);

  20. int[] pArr = new int[charArr.length];

  21. int C = -1;

  22. int R = -1;

  23. int max = Integer.MIN_VALUE;

  24. for (int i = 0; i != charArr.length; i++) {

  25. pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;

  26. while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {

  27. if (charArr[i + pArr[i]] == charArr[i - pArr[i]])

  28. pArr[i]++;

  29. else {

  30. break;

  31. }

  32. }

  33. if (i + pArr[i] > R) {

  34. R = i + pArr[i];

  35. C = i;

  36. }

  37. max = Math.max(max, pArr[i]);

  38. }

  39. return max - 1;

  40. }

  41.  
  42. public static void main(String[] args) {

  43. String str1 = "abc1234321ab";

  44. System.out.println(maxLcpsLength(str1));

  45. }

  46.  
  47. }

问题:查找一个字符串的最长回文子串

首先叙述什么是回文子串:回文:就是对称的字符串,或者说是正反一样的

小问题一:请问,子串和子序列一样么?请思考一下再往下看

 当然,不一样。子序列可以不连续,子串必须连续。

举个例子,123的子串包括1,2,3,12,23,123(一个字符串本身是自己的最长子串),而它的子序列是任意选出元素组成,他的子序列有1,2,3,12,13,23,123,””,空其实也算,但是本文主要是想叙述回文,没意义。

小问题二:长度为n的字符串有多少个子串?多少个子序列?

 子序列,每个元素都可以选或者不选,所以有2的n次方个子序列(包括空)

子串:以一位置开头,有n个子串,以二位置开头,有n-1个子串,以此类推,我们发现,这是一个等差数列,而等差序列求和,有n*(n+1)/2个子串(不包括空)。

(这里有一个思想需要注意,遇到等差数列求和,基本都是o(n^2)级别的)

一、分析枚举的效率

好,我们来分析一下暴力枚举的时间复杂度,上文已经提到过,一个字符串的所有子串,数量是o(n^2)级别,所以光是枚举出所有情况时间就是o(n^2),每一种情况,你要判断他是不是回文的话,还需要o(n),情况数和每种情况的时间,应该乘起来,也就是说,枚举时间要o(n^3),效率太低。

二、初步优化

思路:我们知道,回文全是对称的,每个回文串都会有自己的对称轴,而两边都对称。我们如果从对称轴开始, 向两边阔,如果总相等,就是回文,扩到两边不相等的时候,以这个对称轴向两边扩的最长回文串就找到了。

举例:1 2 1 2 1 2 1 1 1

我们用每一个元素作为对称轴,向两边扩

0位置,左边没东西,只有自己;

1位置,判断左边右边是否相等,1=1所以接着扩,然后左边没了,所以以1位置为对称轴的最长回文长度就是3;

2位置,左右都是2,相等,继续,左右都是1,继续,左边没了,所以最长为5

3位置,左右开始扩,1=1,2=2,1=1,左边没了,所以长度是7

如此把每个对称轴扩一遍,最长的就是答案,对么?

你要是点头了。。。自己扇自己两下。

还有偶回文呢,,比如1221,123321.这是什么情况呢?这个对称轴不是一个具体的数,因为人家是偶回文。

问题三:怎么用对称轴向两边扩的方法找到偶回文?(容易操作的)

我们可以在元素间加上一些符号,比如/1/2/1/2/1/2/1/1/1/,这样我们再以每个元素为对称轴扩就没问题了,每个你加进去的符号都是一个可能的偶数回文对称轴,此题可解。。。因为我们没有错过任何一个可能的对称轴,不管是奇数回文还是偶数回文。

那么请问,加进去的符号,有什么要求么?是不是必须在原字符中没出现过?请思考

 

其实不需要的,大家想一下,不管怎么扩,原来的永远和原来的比较,加进去的永远和加进去的比较。(不举例子说明了,自己思考一下)

好,分析一波时间效率吧,对称轴数量为o(n)级别,每个对称轴向两边能扩多少?最多也就o(n)级别,一共长度才n; 所以n*n是o(n^2)   (最大能扩的位置其实也是两个等差数列,这么理解也是o(n^2),用到刚讲的知识)

 

小结:

这种方法把原来的暴力枚举o(n^3)变成了o(n^2),大家想一想为什么这样更快呢?

我在kmp一文中就提到过,我们写出暴力枚举方法后应想一想自己做出了哪些重复计算,错过了哪些信息,然后进行优化。

看我们的暴力方法,如果按一般的顺序枚举,012345,012判断完,接着判断0123,我是没想到可以利用前面信息的方法,因为对称轴不一样啊,右边加了一个元素,左边没加。所以刚开始,老是想找一种方法,左右都加一个元素,这样就可以对上一次的信息加以利用了。

暴力为什么效率低?永远是因为重复计算,举个例子:12121211,下标从0开始,判断1212121是否为回文串的时候,其实21212和121等串也就判断出来了,但是我们并没有记下结果,当枚举到21212或者121时,我们依旧是重新尝试了一遍。(假设主串长度为n,对称轴越在中间,长度越小的子串,被重复尝试的越多。中间那些点甚至重复了n次左右,本来一次搞定的事)

还是这个例子,我换一个角度叙述一下,比较直观,如果从3号开始向两边扩,121,21212,最后扩到1212121,时间复杂度o(n),用枚举的方法要多少时间?如果主串长度为n,枚举尝试的子串长度为,3,5,7....n,等差数列,大家读到这里应该都知道了,等差数列求和,o(n^2)。

三、Manacher原理

首先告诉大家,这个算法时间可以做到o(n),空间o(n).

好的,开始讲解这个神奇的算法。

首先明白两个概念:

最右回文边界R:挺好理解,就是目前发现的回文串能延伸到的最右端的位置(一个变量解决)

中心c:第一个取得最右回文边界的那个中心对称轴;举个例子:12121,二号元素可以扩到12121,三号元素 可以扩到121,右边界一样,我们的中心是二号元素,因为它第一个到达最右边界

当然,我们还需要一个数组p来记录每一个可能的对称轴最后扩到了哪里。

有了这么几个东西,我们就可以开始这个神奇的算法了。

为了容易理解,我分了四种情况,依次讲解:

 

假设遍历到位置i,如何操作呢

 

1)i>R:也就是说,i以及i右边,我们根本不知道是什么,因为从来没扩到那里。那没有任何优化,直接往右暴力 扩呗。

(下面我们做i关于c的对称点,i

2)i<R:,

三种情况:

i’的回文左边界在c回文左边界的里面

i回文左边界在整体回文的外面

i左边界和c左边界是一个元素

(怕你忘了概念,c是对称中心,c它当初扩到了R,R是目前扩到的最右的地方,现在咱们想以i为中心,看能扩到哪里。)

按原来o(n^2)的方法,直接向两边暴力扩。好的,魔性的优化来了。咱们为了好理解,分情况说。首先,大家应该知道的是,i’其实有人家自己的回文长度,我们用数组p记录了每个位置的情况,所以我们可以知道以i为中心的回文串有多长。

2-1)i’的回文左边界在c回文的里面:看图

我用这两个括号括起来的就是这两个点向两边扩到的位置,也就是i和i’的回文串,为什么敢确定i回文只有这么长?和i一样?我们看c,其实这个图整体是一个回文串啊。

串内完全对称(1是括号左边相邻的元素,2是右括号右边相邻的元素,34同理),

 由此得出结论1:

由整体回文可知,点2=点3,点1=点4

 

当初i’为什么没有继续扩下去?因为点1!=点2。

由此得出结论2:点1!=点2 

 

因为前面两个结论,所以3!=4,所以i也就到这里就扩不动了。而34中间肯定是回文,因为整体回文,和12中间对称。

 

2-2)i回文左边界在整体回文的外面了:看图

这时,我们也可以直接确定i能扩到哪里,请听分析:

当初c的大回文,扩到R为什么就停了?因为点2!=点4----------结论1;

2为2关于i的对称点,当初i左右为什么能继续扩呢?说明点2=点2’---------结论2;

由c回文可知2’=3,由结论2可知点2=点2’,所以2=3;

但是由结论一可知,点2!=点4,所以推出3!=4,所以i扩到34为止了,34不等。

而34中间那一部分,因为c回文,和i在内部的部分一样,是回文,所以34中间部分是回文。

 

2-3)最后一种当然是i左边界和c左边界是一个元素

点1!=点2,点2=点3,就只能推出这些,只知道34中间肯定是回文,外边的呢?不知道啊,因为不知道3和4相不相等,所以我们得出结论:点3点4内肯定是,继续暴力扩。

原理及操作叙述完毕,不知道我讲没讲明白。。。

四、代码及复杂度分析

 看代码大家是不是觉得不像o(n)?其实确实是的,来分析一波。。

首先,我们的i依次往下遍历,而R(最右边界)从来没有回退过吧?其实当我们的R到了最右边,就可以结束了。再不济i自己也能把R一个一个怼到最右

我们看情况一和四,R都是以此判断就向右一个,移动一次需要o(1)

我们看情况二和三,直接确定了p[i],根本不用扩,直接遍历下一个元素去了,每个元素o(1).

综上,由于i依次向右走,而R也没有回退过,最差也就是i和R都到了最右边,而让它们移动一次的代价都是o(1)的,所以总体o(n)

可能大家看代码依旧有点懵,其实就是code整合了一下,我们对于情况23,虽然知道了它肯定扩不动,但是我们还是给它一个起码是回文的范围,反正它扩一下就没扩动,不影响时间效率的。而情况四也一样,给它一个起码是回文,不用验证的区域,然后接着扩,四和二三的区别就是。二三我们已经心中有B树,它肯定扩不动了,而四确实需要接着尝试。

(要是写四种情况当然也可以。。但是我懒的写,太多了。便于理解分了四种情况解释,code整合后就是这样子)

 

字数3411

2017/12/22

 

 

前缀树

是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

字典树又称为前缀树或Trie树,是处理字符串常见的数据结构。假设组成所有单词的字符仅是“a”~"z",请实现字典树结构,并包含以下四个主要功能:

void insert(String word):添加word,可重复添加。
void delete(String word):删除word,如果word添加过多次,仅删除一次。
boolean search(String word):查询word是否在字典树中。
int prefixNumber(String pre):返回以字符串pre为前缀的单词数量。
思考:

字典树的介绍。字典树是一种树形结构,优点是利用字符串的公共前缀来节约存储空间。

 

基本性质:

字典树的基本性质如下:

  • 根节点没有字符路径。除根节点外,每一个节点都被一个字符路径找到。
  • 从根节点到某一节点,将路径上经过的字符连接起来,为扫过的对应字符串。
  • 每个节点向下所有的字符路径上的字符都不同。

也不需要记,看了实现,很自然的性质就理解了。

每个结点内有一个指针数组,里面有二十六个指针,分别指向二十六个字母。

如果指向某个字母的指针为空,那就是以前没有遇到过这个前缀。

 

搜索的方法为:

(1) 从根结点开始一次搜索;

(2) 取得要查找关键词的第一个字母,并根据该字母选择对应的子树并转到该子树继续进行检索;

(3) 在相应的子树上,取得要查找关键词的第二个字母,并进一步选择对应的子树进行检索。

(4) 迭代过程……

(5) 在某个结点处,关键词的所有字母已被取出,则读取附在该结点上的信息,即完成查找。

其他操作类似处理

插入也一样,只是转到某个子树时,没有子树,那就创建一个新节点,然后对应指针指向新节点即可。

我们给出定义就更清楚了:

 
  1. public static class TrieNode {

  2. public int path; //表示由多少个字符串共用这个节点

  3. public int end;//表示有多少个字符串是以这个节点结尾的

  4. public TrieNode[] map;

  5. //哈希表结构,key代表该节点的一条字符路径,value表示字符路径指向的节点

  6. public TrieNode() {

  7. path = 0;

  8. end = 0;

  9. map = new TrieNode[26];

  10. }

  11. }

path和end都是有用的,接下来会说明

insert:

 
  1. public static class Trie {

  2. private TrieNode root;//头

  3.  
  4. public Trie() {

  5. root = new TrieNode();

  6. }

  7.  
  8. public void insert(String word) {

  9. if (word == null) {

  10. return;

  11. }//空串

  12. char[] chs = word.toCharArray();

  13. TrieNode node = root;

  14. int index = 0; //哪条路

  15. for (int i = 0; i < chs.length; i++) {

  16. index = chs[i] - 'a'; //0~25

  17. if (node.map[index] == null) {

  18. node.map[index] = new TrieNode();

  19. }//创建,继续

  20. node = node.map[index];//指向子树

  21. node.path++;//经过加1

  22. }

  23. node.end++;//本单词个数加1

  24. }

 
  1. public boolean search(String word) {

  2. if (word == null) {

  3. return false;

  4. }

  5. char[] chs = word.toCharArray();

  6. TrieNode node = root;

  7. int index = 0;

  8. for (int i = 0; i < chs.length; i++) {

  9. index = chs[i] - 'a';

  10. if (node.map[index] == null) {

  11. return false;//找不到

  12. }

  13. node = node.map[index];

  14. }

  15. return node.end != 0;//end标记有没有以这个字符为结尾的字符串

  16. }

delete: 

 
  1. public void delete(String word) {

  2. //如果有

  3. if (search(word)) {

  4. char[] chs = word.toCharArray();

  5. TrieNode node = root;

  6. int index = 0;

  7. for (int i = 0; i < chs.length; i++) {

  8. index = chs[i] - 'a';

  9. if (node.map[index].path-- == 1) {//path减完之后为0

  10. node.map[index] = null;

  11. return;

  12. }

  13. node = node.map[index];//去子树

  14. }

  15. node.end--;//次数减1

  16. }

  17. }

prefixNumber:

 
  1. public int prefixNumber(String pre) {

  2. if (pre == null) {

  3. return 0;

  4. }

  5. char[] chs = pre.toCharArray();

  6. TrieNode node = root;

  7. int index = 0;

  8. for (int i = 0; i < chs.length; i++) {

  9. index = chs[i] - 'a';

  10. if (node.map[index] == null) {

  11. return 0;//找不到

  12. }

  13. node = node.map[index];

  14. }

  15. return node.path;//返回经过的次数即可

  16. }

好处:

1.利用字符串的公共前缀来节约存储空间。

2.最大限度地减少无谓的字符串比较,查询效率比较高。例如:若要查找的字符长度是5,而总共有单词的数目是26^5=11881376,利用trie树,利用5次比较可以从11881376个可能的关键字中检索出指定的关键字,而利用二叉查找树时间复杂度是O( log2n ),所以至少要进行log211881376=23.5次比较。可以看出来利用字典树进行查找速度是比较快的。

 

应用:

<1.字符串的快速检索

<2.字符串排序

<3.最长公共前缀:abdh和abdi的最长公共前缀是abd,遍历字典树到字母d时,此时这些单词的公共前缀是abd。

<4.自动匹配前缀显示后缀

我们使用辞典或者是搜索引擎的时候,输入appl,后面会自动显示一堆前缀是appl的东东吧。

那么有可能是通过字典树实现的,前面也说了字典树可以找到公共前缀,我们只需要把剩余的后缀遍历显示出来即可。

 

相关题目:

一个字符串类型的数组arr1,另一个字符串类型的数组arr2。

arr2中有哪些字符,是arr1中出现的?请打印。

arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。

arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印arr2中出现次数最大的前缀。

 

后缀树/后缀数组

字典树:https://blog.csdn.net/hebtu666/article/details/83141560

后缀树:后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。

 

相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等。

比如单词banana,它的所有后缀显示到下面的。0代表从第一个字符为起点,终点不用说都是字符串的末尾。

以上面的后缀,我们建立一颗后缀树。如下图,为了方便看到后缀,我没有合并相同的前缀。

把非公共部分压缩:

后缀树的应用:

(1)查找某个字符串s1是否在另外一个字符串s2中:如果s1在字符串s2中,那么s1必定是s2中某个后缀串的前缀。

(2)指定字符串s1在字符串s2中重复的次数:比如说banana是s1,an是s2,那么计算an出现的次数实际上就是看an是几个后缀串的前缀。

(3)两个字符串S1,S2的最长公共部分(广义后缀树)

(4)最长回文串(广义后缀树)

 

关于后缀树的实现和应用以后再写,这次主要写后缀数组。

在字符串处理当中,后缀树和后缀数组都是非常有力的工具。其实后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也不太逊色,并且,它比后缀树所占用的空间小很多。可以说,在信息学竞赛中后缀数组比后缀树要更为实用。

 

后缀数组:就是把某个字符串的所有后缀按照字典序排序后的数组。(数组中保存起始位置就好了,结束位置一定是最后)

先说如何计算后缀数组:

倍增的思想,我们先把每个长度为2的子串排序,再利用结果把每个长度为4的字串排序,再利用结果排序长度为8的子串。。。直到长度大于等于串长。

设置sa[]数组来记录排名:sa[i]代表排第i名的是第几个串。

结果用rank[]数组返回,rank[i]记录的是起始位置为第i个字符的后缀排名第几小。

我们开始执行过程:

比如字符串abracadabra

长度为2的排名:a ab ab ac ad br br ca da ra ra,他们分别排第0,1,2,2,3,4,5,5,6,7,8,8名

sa数组就是11(空串),10(a),0(ab),7,3,5,1,8,4,6,2,9(ra排名最后)

这样,所有长度为2的子串的排名就出来了,我们如何利用排名把长度为4的排名搞出来呢?

abracadabra中,ab,br,ra这些串排名知道了。我们把他们两两合并为长度为4的串,进行排名。

比如abra和brac怎么比较呢?

用原来排名的数对来表示

abra=ab+ra=1+8

brac=br+ac=4+2

对于字符串的字典序,这个例子比1和4就比出来了。

如果第一个数一样,也就是前两个字符一样,那再比后面就可以了。

简单说就是先比前一半字符的排名,再比后一半的排名。

具体实现,我们可以用系统sort,传一个比较器就好了。

 

还有需要注意,长度不可能那么凑巧是2^n,所以 一般的,k=n时,rank[i]表示从位置i开始向后n个字符的排名第几小,而剩下不足看个字符,rank[i]代表从第i个字符到最后的串的排名第几小,也就是后缀。

保证了每一个后缀都能正确表示并排序。比如k=4时,就表示出了长度为1,2,3的后缀:a,ra,bra.这就保证了k=8时,长度为5,6,7的后缀也能被表示出来:4+1,4+2,4+3

还有,sa[0]永远是空串,空串的排名rank[sa[0]]永远是最大。

 
  1. int n;

  2. int k;

  3. int rank[MAX_N+1];//结果(排名)数组

  4. int tmp[MAX_N+1];//临时数组

  5. //定义比较器

  6. bool compare(int i,int j)

  7. {

  8. if(rank[i]!=rank[j])return rank[i]<rank[j];

  9. //长度为k的子串的比较

  10. int ri=i+k<=n ? rank[i+k] : -1;

  11. int rj=j+k<=n ? rank[j+k] : -1;

  12. return ri<rj;

  13. }

  14.  
  15. void solve(string s,int *sa)

  16. {

  17. n=s.length;

  18. //长度为1时,按字符码即可,长度为2时就可以直接用

  19. for(int i=0;i<=n;i++)

  20. {

  21. sa[i]=i;

  22. rank[i]=i<n ? s[i] : -1;//注意空串为最大

  23. }

  24. //由k对2k排序,直到超范围

  25. for(k=1;k<=n;k*=2)

  26. {

  27. sort(sa,sa+n+1,compare);

  28. tmp[sa[0]=0;//空串

  29. for(int i=1;i<=n;i++)

  30. {

  31. tmp[sa[i]]=tmp[sa[i-1]]+(compare(sa[i-1],sa[i]) ? 1 : 0);//注意有相同的

  32. }

  33. for(int i=0;i<=n;i++)

  34. {

  35. rank[i]=tmp[i];

  36. }

  37. }

  38. }

具体应用以后再写。。。。。

 

AC自动机

今天写一下基本的AC自动机的思想原理和实现。

Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。

KMP算法是单模式串的字符匹配算法,AC自动机是多模式串的字符匹配算法。

首先我们回忆一下KMP算法:失配之后,子串通过next数组找到应该匹配的位置,也就是最长相等前后缀。

AC自动机也是一样,只不过是匹配到当前失配之后,找到当前字符串的后缀,和所有字符串的前缀,找出最长相等前后缀。

就这么简单。

当然,字典树的知识是需要了解的。

我就默认读者都会字典树了。

我们操作的第一步就是把那些单词做一个字典树出来,这个好理解。

 

在AC自动机中,我们也有类似next数组的东西就是fail指针,当发现失配的字符失配的时候,跳转到fail指针指向的位置,然后再次进行匹配操作

当前节点t有fail指针,其fail指针所指向的节点和t所代表的字符是相同的。因为t匹配成功后,我们需要去匹配t->child,发现失配,那么就从t->fail这个节点开始再次去进行匹配。

KMP里有详细讲解过程,我就不占篇幅叙述了。

然后说一下fail指针如何建立:

和next数组大同小异。如果你很熟悉next数组的建立,fail指针也是一样的。

假设当前节点为father,其孩子节点记为child。求child的Fail指针时,首先我们要找到其father的Fail指针所指向的节点,假如是t的话,我们就要看t的孩子中有没有和child节点所表示的字母相同的节点,如果有的话,这个节点就是child的fail指针,如果发现没有,则需要找father->fail->fail这个节点,然后重复上面过程,如果一直找都找不到,则child的Fail指针就要指向root。

KMP也是一样的的操作:p[next[i-1]]p[next[next[i-1]]]这样依次往前跳啊。

 

如果跳转,跳转后的串的前缀,必为跳转前的模式串的后缀并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点。所以我们可以利用 bfs在 Trie上面进行 fail指针的求解。流程和NEXT数组类似。

 

匹配的时候流程也是基本一样的,请参考KMP或者直接看代码:

HDU 2222 Keywords Search    最基本的入门题了

就是求目标串中出现了几个模式串。

很基础了。使用一个int型的end数组记录,查询一次。

 
  1. #include <stdio.h>

  2. #include <algorithm>

  3. #include <iostream>

  4. #include <string.h>

  5. #include <queue>

  6. using namespace std;

  7.  
  8. struct Trie

  9. {

  10. int next[500010][26],fail[500010],end[500010];

  11. int root,L;

  12. int newnode()

  13. {

  14. for(int i = 0;i < 26;i++)

  15. next[L][i] = -1;

  16. end[L++] = 0;

  17. return L-1;

  18. }

  19. void init()

  20. {

  21. L = 0;

  22. root = newnode();

  23. }

  24. void insert(char buf[])

  25. {

  26. int len = strlen(buf);

  27. int now = root;

  28. for(int i = 0;i < len;i++)

  29. {

  30. if(next[now][buf[i]-'a'] == -1)

  31. next[now][buf[i]-'a'] = newnode();

  32. now = next[now][buf[i]-'a'];

  33. }

  34. end[now]++;

  35. }

  36. void build()//建树

  37. {

  38. queue<int>Q;

  39. fail[root] = root;

  40. for(int i = 0;i < 26;i++)

  41. if(next[root][i] == -1)

  42. next[root][i] = root;

  43. else

  44. {

  45. fail[next[root][i]] = root;

  46. Q.push(next[root][i]);

  47. }

  48. while( !Q.empty() )//建fail

  49. {

  50. int now = Q.front();

  51. Q.pop();

  52. for(int i = 0;i < 26;i++)

  53. if(next[now][i] == -1)

  54. next[now][i] = next[fail[now]][i];

  55. else

  56. {

  57. fail[next[now][i]]=next[fail[now]][i];

  58. Q.push(next[now][i]);

  59. }

  60. }

  61. }

  62. int query(char buf[])//匹配

  63. {

  64. int len = strlen(buf);

  65. int now = root;

  66. int res = 0;

  67. for(int i = 0;i < len;i++)

  68. {

  69. now = next[now][buf[i]-'a'];

  70. int temp = now;

  71. while( temp != root )

  72. {

  73. res += end[temp];

  74. end[temp] = 0;

  75. temp = fail[temp];

  76. }

  77. }

  78. return res;

  79. }

  80. void debug()

  81. {

  82. for(int i = 0;i < L;i++)

  83. {

  84. printf("id = %3d,fail = %3d,end = %3d,chi = [",i,fail[i],end[i]);

  85. for(int j = 0;j < 26;j++)

  86. printf("%2d",next[i][j]);

  87. printf("]\n");

  88. }

  89. }

  90. };

  91. char buf[1000010];

  92. Trie ac;

  93. int main()

  94. {

  95. int T;

  96. int n;

  97. scanf("%d",&T);

  98. while( T-- )

  99. {

  100. scanf("%d",&n);

  101. ac.init();

  102. for(int i = 0;i < n;i++)

  103. {

  104. scanf("%s",buf);

  105. ac.insert(buf);

  106. }

  107. ac.build();

  108. scanf("%s",buf);

  109. printf("%d\n",ac.query(buf));

  110. }

  111. return 0;

  112. }

 

数组缺失

 

二叉树遍历

二叉树:二叉树是每个节点最多有两个子树的树结构。

 

本文介绍二叉树的遍历相关知识。

我们学过的基本遍历方法,无非那么几个:前序,中序,后序,还有按层遍历等等。

设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。

首先我们定义一颗二叉树

 
  1. typedef char ElementType;

  2. typedef struct TNode *Position;

  3. typedef Position BinTree;

  4. struct TNode{

  5. ElementType Data;

  6. BinTree Left;

  7. BinTree Right;

  8. };

前序

首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树

思路:

就是利用函数,先打印本个节点,然后对左右子树重复此过程即可。

 
  1. void PreorderTraversal( BinTree BT )

  2. {

  3. if(BT==NULL)return ;

  4. printf(" %c", BT->Data);

  5. PreorderTraversal(BT->Left);

  6. PreorderTraversal(BT->Right);

  7. }

 

中序

首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树

思路:

还是利用函数,先对左边重复此过程,然后打印根,然后对右子树重复。

 
  1. void InorderTraversal( BinTree BT )

  2. {

  3. if(BT==NULL)return ;

  4. InorderTraversal(BT->Left);

  5. printf(" %c", BT->Data);

  6. InorderTraversal(BT->Right);

  7. }

后序

首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根

思路:

先分别对左右子树重复此过程,然后打印根

 
  1. void PostorderTraversal(BinTree BT)

  2. {

  3. if(BT==NULL)return ;

  4. PostorderTraversal(BT->Left);

  5. PostorderTraversal(BT->Right);

  6. printf(" %c", BT->Data);

  7. }

进一步思考

看似好像很容易地写出了三种遍历。。。。。

 

但是你真的理解为什么这么写吗?

比如前序遍历,我们真的是按照定义里所讲的,首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树。这种过程来遍历了一遍二叉树吗?

仔细想想,其实有一丝不对劲的。。。

再看代码:

 
  1. void Traversal(BinTree BT)//遍历

  2. {

  3. //1111111111111

  4. Traversal(BT->Left);

  5. //22222222222222

  6. Traversal(BT->Right);

  7. //33333333333333

  8. }

为了叙述清楚,我给三个位置编了号 1,2,3

我们凭什么能前序遍历,或者中序遍历,后序遍历?

我们看,前序中序后序遍历,实现的代码其实是类似的,都是上面这种格式,只是我们分别在位置1,2,3打印出了当前节点而已啊。我们凭什么认为,在1打印,就是前序,在2打印,就是中序,在3打印,就是后序呢?不管在位置1,2,3哪里操作,做什么操作,我们利用函数遍历树的顺序变过吗?当然没有啊。。。

都是三次返回到当前节点的过程:先到本个节点,也就是位置1,然后调用了其他函数,最后调用完了,我们开到了位置2。然后又调用别的函数,调用完了,我们来到了位置3.。然后,最后操作完了,这个函数才结束。代码里的三个位置,每个节点都被访问了三次。

而且不管位置1,2,3打印了没有,操作了没有,这个顺序是永远存在的,不会因为你在位置1打印了,顺序就改为前序,你在位置2打印了,顺序就成了中序。

 

为了有更直观的印象,我们做个试验:在位置1,2,3全都放入打印操作;

我们会发现,每个节点都被打印了三次。而把每个数第一次出现拿出来,就组成了前序遍历的序列;所有数字第二次出现拿出来,就组成了中序遍历的序列。。。。

 

其实,遍历是利用了一种数据结构:栈

而我们这种写法,只是通过函数,来让系统帮我们压了栈而已。为什么能实现遍历?为什么我们访问完了左子树,能返回到当前节点?这都是栈的功劳啊。我们把当前节点(对于函数就是当时的现场信息)存到了栈里,记录下来,后来才能把它拿了出来,能回到以前的节点。

 

想到这里,可能就有更深刻的理解了。

我们能否不用函数,不用系统帮我们压栈,而是自己做一个栈,来实现遍历呢?

先序实现思路:拿到一个节点的指针,先判断是否为空,不为空就先访问(打印)该结点,然后直接进栈,接着遍历左子树;为空则要从栈中弹出一个节点来,这个时候弹出的结点就是其父亲,然后访问其父亲的右子树,直到当前节点为空且栈为空时,结束。

核心思路代码实现:

 
  1. *p=root;

  2. while(p || !st.empty())

  3. {

  4. if(p)//非空

  5. {

  6. //visit(p);进行操作

  7. st.push(p);//入栈

  8. p = p->lchild;左

  9. }

  10. else//空

  11. {

  12. p = st.top();//取出

  13. st.pop();

  14. p = p->rchild;//右

  15. }

  16. }

中序实现思路:和前序遍历一样,只不过在访问节点的时候顺序不一样,访问节点的时机是从栈中弹出元素时访问,如果从栈中弹出元素,就意味着当前节点父亲的左子树已经遍历完成,这时候访问父亲,就是中序遍历.

(对应递归是第二次遇到)

核心代码实现:

 
  1. *p=root;

  2. while(p || !st.empty())

  3. {

  4. if(p)//非空

  5. {

  6. st.push(p);//压入

  7. p = p->lchild;

  8. }

  9. else//空

  10. {

  11. p = st.top();//取出

  12. //visit(p);操作

  13. st.pop();

  14. p = p->rchild;

  15. }

  16. }

后序遍历是最难的。因为要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难点。

因为我们原来说了,后序是第三次遇到才进行操作的,所以我们很容易有这种和递归函数类似的思路:对于任一结点,将其入栈,然后沿其左子树一直往下走,一直走到没有左孩子的结点,此时该结点在栈顶,但是不能出栈访问, 因此右孩子还没访问。所以接下来按照相同的规则对其右子树进行相同的处理。访问完右孩子,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。

第二种思路:对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,或者左孩子和右孩子都已被访问过了,就可以直接访问该结点。如果有孩子未访问,将P的右孩子和左孩子依次入栈。

网上的思路大多是第一种,所以我在这里给出第二种的大概实现吧

首先初始化cur,pre两个指针,代表访问的当前节点和之前访问的节点。把根放入,开始执行。

 
  1. s.push(root);

  2. while(!s.empty())

  3. {

  4. cur=s.top();

  5. if((cur->lchild==NULL && cur->rchild==NULL)||(pre!=NULL && (pre==cur->lchild||pre==cur->rchild)))

  6. {

  7. //visit(cur); 如果当前结点没有孩子结点或者孩子节点都已被访问过

  8. s.pop();//弹出

  9. pre=cur; //记录

  10. }

  11. else//分别放入右左孩子

  12. {

  13. if(cur->rchild!=NULL)

  14. s.push(cur->rchild);

  15. if(cur->lchild!=NULL)

  16. s.push(cur->lchild);

  17. }

  18. }

这两种方法,都是利用栈结构来实现的遍历,需要一定的栈空间,而其实存在一种时间O(N),空间O(1)的遍历方式,下次写了我再放链接。

 

斗个小机灵:后序是LRD,我们其实已经知道先序是DLR,那其实我们可以用先序来实现后序啊,我们只要先序的时候把左右子树换一下:DRL(这一步很好做到),然后倒过来不就是DRL了嘛。。。。。就把先序代码改的左右反过来,然后放栈里倒过来就好了,不需要上面介绍的那些复杂的方法。。。。

 

二叉树序列化/反序列化

二叉树被记录成文件的过程,为二叉树的序列化

通过文件重新建立原来的二叉树的过程,为二叉树的反序列化

设计方案并实现。

(已知结点类型为32位整型)

 

思路:先序遍历实现。

因为要写入文件,我们要把二叉树序列化为一个字符串。

首先,我们要规定,一个结点结束后的标志:“!”

然后就可以通过先序遍历生成先序序列了。

 

但是,众所周知,只靠先序序列是无法确定一个唯一的二叉树的,原因分析如下:

比如序列1!2!3!

我们知道1是根,但是对于2,可以作为左孩子,也可以作为右孩子:

对于3,我们仍然无法确定,应该作为左孩子还是右孩子,情况显得更加复杂:

原因:我们对于当前结点,插入新结点是无法判断插入位置,是应该作为左孩子,还是作为右孩子。

因为我们的NULL并未表示出来。

如果我们把NULL也用一个符号表示出来:

比如

1!2!#!#!3!#!#!

我们再按照先序遍历的顺序重建:

对于1,插入2时,就确定要作为左孩子,因为左孩子不为空。

然后接下来两个#,我们就知道了2的左右孩子为空,然后重建1的右子树即可。

 

我们定义结点:

 
  1. public static class Node {

  2. public int value;

  3. public Node left;

  4. public Node right;

  5.  
  6. public Node(int data) {

  7. this.value = data;

  8. }

  9. }

序列化:

 
  1. public static String serialByPre(Node head) {

  2. if (head == null) {

  3. return "#!";

  4. }

  5. String res = head.value + "!";

  6. res += serialByPre(head.left);

  7. res += serialByPre(head.right);

  8. return res;

  9. }

 

 
  1. public static Node reconByPreString(String preStr) {

  2. //先把字符串转化为结点序列

  3. String[] values = preStr.split("!");

  4. Queue<String> queue = new LinkedList<String>();

  5. for (int i = 0; i != values.length; i++) {

  6. queue.offer(values[i]);

  7. }

  8. return reconPreOrder(queue);

  9. }

  10.  
  11. public static Node reconPreOrder(Queue<String> queue) {

  12. String value = queue.poll();

  13. if (value.equals("#")) {

  14. return null;//遇空

  15. }

  16. Node head = new Node(Integer.valueOf(value));

  17. head.left = reconPreOrder(queue);

  18. head.right = reconPreOrder(queue);

  19. return head;

  20. }

这样并未改变先序遍历的时空复杂度,解决了先序序列确定唯一一颗树的问题,实现了二叉树序列化和反序列化。

 

先序中序后序两两结合重建二叉树

遍历是对树的一种最基本的运算,所谓遍历二叉树,就是按一定的规则和顺序走遍二叉树的所有结点,使每一个结点都被访问一次,而且只被访问一次。由于二叉树是非线性结构,因此,树的遍历实质上是将二叉树的各个结点转换成为一个线性序列来表示。

设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。

先序遍历

首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树,C语言代码如下:

1

2

3

4

5

6

7

void XXBL(tree *root){

    //DoSomethingwithroot

    if(root->lchild!=NULL)

        XXBL(root->lchild);

    if(root->rchild!=NULL)

        XXBL(root->rchild);

}

中序遍历

首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树,C语言代码如下

1

2

3

4

5

6

7

8

void ZXBL(tree *root)

{

    if(root->lchild!=NULL)

        ZXBL(root->lchild);

        //Do something with root

    if(root->rchild!=NULL)

        ZXBL(root->rchild);

}

后序遍历

首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根,C语言代码如下

1

2

3

4

5

6

7

void HXBL(tree *root){

    if(root->lchild!=NULL)

        HXBL(root->lchild);

    if(root->rchild!=NULL)

        HXBL(root->rchild);

        //Do something with root

}

层次遍历

即按照层次访问,通常用队列来做。访问根,访问子女,再访问子女的子女(越往后的层次越低)(两个子女的级别相同)

 

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

 

我们首先找到根结点:一定是先序遍历序列的第一个元素:1

然后,在中序序列寻找根,把中序序列分为两个序列左子树4,7,2和右子树5,3,8,6

把先序序列也分为两个:                                           左子树2,4,7和右子树3,5,6,8

对左右重复同样的过程:

先看左子树:先序序列4,7,2,说明4一定是左子树的根

把2,4,7分为2和7两个序列,再重复过程,左边确定完毕。

右子树同样:中序序列为5,3,8,6,先序序列为:3,5,6,8

取先序头,3.一定是根

把中序序列分为     5和8,6两个序列

对应的先序序列为 5和6,8两个序列

 

然后确定了5是3的左孩子

对于先序序列6,8和中序序列8,6

还是先取先序的头,6

 

现在只有8,中序序列8在左边,是左孩子。

结束。

我们总结一下这种方法的过程:

1、根据先序序列确定当前树的根(第一个元素)。

2、在中序序列中找到根,并以根为分界分为两个序列。

3、这样,确定了左子树元素个数,把先序序列也分为两个。

对左右子树(对应的序列)重复相同的过程。

 

我们把思路用代码实现:

 
  1. # -*- coding:utf-8 -*-

  2. # class TreeNode:

  3. # def __init__(self, x):

  4. # self.val = x

  5. # self.left = None

  6. # self.right = None

  7. class Solution:

  8. # 返回构造的TreeNode根节点

  9. def reConstructBinaryTree(self, pre, tin):

  10. # write code here/

  11. #pre-先序数组 tin->中序数组

  12. if len(pre) == 0:

  13. return None

  14. root = TreeNode(pre[0])//第一个元素为根

  15. pos = tin.index(pre[0])//划分左右子树

  16. root.left = self.reConstructBinaryTree( pre[1:1+pos], tin[:pos])

  17. root.right = self.reConstructBinaryTree( pre[pos+1:], tin[pos+1:])

  18. return root

输入某二叉树的后序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

 

思路是类似的,只是我们确定根的时候,取后序序列的最后一个元素即可。

 

输入某二叉树的后序遍历和先序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

 

我们直白的表述一下,前序是中左右,后序是左右中。

所以,我们凭先序和后序序列其实是无法判断根的孩子到底是左孩子还是右孩子。

比如先序序列1,5,后序序列是5,1

我们只知道1是这棵树的根,但是我们不知道5是1的左孩子还是右孩子。

我们的中序序列是左中右,才可以明确的划分出左右子树,而先序后序不可以。

 

综上,只有,只含叶子结点或者同时有左右孩子的结点的树,才可以被先序序列后序序列确定唯一一棵树。

最后不断划分先序和后序序列完成重建。

 

先序中序数组推后序数组

二叉树遍历

所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问 题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。

 

从二叉树的递归定义可知,一棵非空的二叉树由根结点及左、右子树这三个基本部分组成。因此,在任一给定结点上,可以按某种次序执行三个操作:

⑴访问结点本身(N),

⑵遍历该结点的左子树(L),

⑶遍历该结点的右子树(R)。

以上三种操作有六种执行次序:

NLR、LNR、LRN、NRL、RNL、RLN。

注意:

前三种次序与后三种次序对称,故只讨论先左后右的前三种次序。

遍历命名

根据访问结点操作发生位置命名:

① NLR:前序遍历(Preorder Traversal 亦称(先序遍历))

——访问根结点的操作发生在遍历其左右子树之前。

② LNR:中序遍历(Inorder Traversal)

——访问根结点的操作发生在遍历其左右子树之中(间)。

③ LRN:后序遍历(Postorder Traversal)

——访问根结点的操作发生在遍历其左右子树之后。

注意:

由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

 

给出某棵树的先序遍历结果和中序遍历结果(无重复值),求后序遍历结果。

比如

先序序列为:1,2,4,5,3,6,7,8,9

中序序列为:4,2,5,1,6,3,7,9,8

方法1:我们可以重建整棵树:

https://blog.csdn.net/hebtu666/article/details/84322113

建议好好看这个网址,对理解这个方法有帮助。

 

如图

然后后序遍历得出后序序列。

 

方法2:我们可以不用重建,直接得出:

过程:

1)根据当前先序数组,设置后序数组最右边的值

2)划分出左子树的先序、中序数组和右子树的先序、中序数组

3)对右子树重复同样的过程

4)对左子树重复同样的过程

 

原因:我们的后序遍历是左右中的,也就是先左子树,再右子树,再根

举个例子:

比如这是待填充序列:

我们确定了根,并且根据根和中序序列划分出了左右子树,黄色部分为左子树:

先处理右子树(其实左右中反过来就是中右左,顺着填就好了):

我们又确定了右子树的右子树为黑色区域,然后接着填右子树的右子树的根(N)即可。

 

 

举例说明:

a[]先序序列为:1,2,4,5,3,6,7,8,9

b[]中序序列为:4,2,5,1,6,3,7,9,8

c[]后序序列为:0,0,0,0,0,0,0,0,0(0代表未确定)

我们根据先序序列,知道根一定是1,所以后序序列:0,0,0,0,0,0,0,0,1

从b[]中找到1,并划分数组:

          左子树的先序:2,4,5,

          中序:4,2,5

          右子树的先序:3,6,7,8,9,

          中序:6,3,7,9,8

 

我们继续对右子树重复相同的过程:

(图示为当前操作的树,我们是不知道这棵树的样子的,我是为了方便叙述,图片表达一下当前处理的位置)

当前树的根一定为先序序列的第一个元素,3,所以我们知道后序序列:0,0,0,0,0,0,0,3,1

我们继续对左右子树进行划分,中序序列为6,3,7,9,8,我们在序列中找到2,并划分为左右子树:

左子树:

先序序列:6

中序序列:6

右子树:

先序序列:7,8,9

中序序列:7,9,8

我们继续对右子树重复相同的过程,也就是如图所示的这棵树:

现在我们的后序序列为0,0,0,0,0,0,0,3,1

这时我们继续取当前的根(先序第一个元素)放在下一个后序位置:0,0,0,0,0,0,7,3,1

划分左右子树:

左子树:空,也就是它

右子树:先序8,9,中序9,8,也就是这个树

我们继续处理右子树:先序序列为8,9,所以根为8,我们继续填后序数组0,0,0,0,0,8,7,3,1

然后划分左右子树:

左子树:先序:9,中序:9

右子树:空

对于左子树,一样,我们取头填后序数组0,0,0,0,9,8,7,3,1,然后发现左右子树都为空.

我们就把这个小框框处理完了

然后这棵树的右子树就处理完了,处理左子树,发现为空。这棵树也处理完了。

这一堆就完了。我们处理以3为根的二叉树的左子树。继续填后序数组:

0,0,0,6,9,8,7,3,1

整棵树的右子树处理完了,左子树同样重复这个过程。

最后4,5,2,6,9,8,7,3,1

 

好累啊。。。。。。挺简单个事写了这么多。

回忆一下过程:

1)根据当前先序数组,设置后序数组最右边的值

2)划分出左子树的先序、中序数组和右子树的先序、中序数组

3)对右子树重复同样的过程

4)对左子树重复同样的过程

就这么简单

 

先填右子树是为了数组连续填充,容易理解,先处理左子树也可以。

最后放上代码吧

 
  1. a=[1,2,4,5,3,6,7,8,9]

  2. b=[4,2,5,1,6,3,7,9,8]

  3. l=[0,0,0,0,0,0,0,0,0]

  4.  
  5. def f(pre,tin,x,y):

  6. #x,y为树在后序数组中对应的范围

  7. if pre==[]:return

  8. l[y]=pre[0]#根

  9. pos=tin.index(pre[0])#左子树元素个数

  10. f(pre[pos+1:],tin[pos+1:],x+pos,y-1)#处理右子树

  11. f(pre[1:pos+1],tin[:pos],x,x+pos-1)#处理左子树

  12.  
  13. f(a,b,0,len(l)-1)

  14. print(l)

根据数组建立平衡二叉搜索树

它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉(搜索)树。

 

二分:用有序数组中中间的数生成搜索二叉树的头节点,然后对数组的左右部分分别生成左右子树即可(重复过程)。

生成的二叉树中序遍历一定还是这个序列。

 

非常简单,不过多叙述:

 
  1. public class SortedArrayToBalancedBST {

  2.  
  3. public static class Node {

  4. public int value;

  5. public Node left;

  6. public Node right;

  7.  
  8. public Node(int data) {

  9. this.value = data;

  10. }

  11. }

  12.  
  13. public static Node generateTree(int[] sortArr) {

  14. if (sortArr == null) {

  15. return null;

  16. }

  17. return generate(sortArr, 0, sortArr.length - 1);

  18. }

  19.  
  20. public static Node generate(int[] sortArr, int start, int end) {

  21. if (start > end) {

  22. return null;

  23. }

  24. int mid = (start + end) / 2;

  25. Node head = new Node(sortArr[mid]);

  26. head.left = generate(sortArr, start, mid - 1);

  27. head.right = generate(sortArr, mid + 1, end);

  28. return head;

  29. }

  30.  
  31. // for test -- print tree

  32. public static void printTree(Node head) {

  33. System.out.println("Binary Tree:");

  34. printInOrder(head, 0, "H", 17);

  35. System.out.println();

  36. }

  37.  
  38. public static void printInOrder(Node head, int height, String to, int len) {

  39. if (head == null) {

  40. return;

  41. }

  42. printInOrder(head.right, height + 1, "v", len);

  43. String val = to + head.value + to;

  44. int lenM = val.length();

  45. int lenL = (len - lenM) / 2;

  46. int lenR = len - lenM - lenL;

  47. val = getSpace(lenL) + val + getSpace(lenR);

  48. System.out.println(getSpace(height * len) + val);

  49. printInOrder(head.left, height + 1, "^", len);

  50. }

  51.  
  52. public static String getSpace(int num) {

  53. String space = " ";

  54. StringBuffer buf = new StringBuffer("");

  55. for (int i = 0; i < num; i++) {

  56. buf.append(space);

  57. }

  58. return buf.toString();

  59. }

  60.  
  61. public static void main(String[] args) {

  62. int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

  63. printTree(generateTree(arr));

  64.  
  65. }

  66.  
  67. }

java整体打印二叉树

一个调的很好的打印二叉树的代码。

用空格和^v来表示节点之间的关系。

效果是这样:

Binary Tree:
                                         v7v       
                        v6v       
                                         ^5^       
       H4H       
                                         v3v       
                        ^2^       
                                         ^1^  

 

对于每个节点,先打印右子树,然后打印本身,然后打印左子树。

 

 
  1. public class fan {

  2. public static class Node {

  3. public int value;

  4. Node left;

  5. Node right;

  6.  
  7. public Node(int data) {

  8. this.value = data;

  9. }

  10. }

  11.  
  12. public static void printTree(Node head) {

  13. System.out.println("Binary Tree:");

  14. printInOrder(head, 0, "H", 17);

  15. System.out.println();

  16. }

  17.  
  18. public static void printInOrder(Node head, int height, String to, int len) {

  19. if (head == null) {

  20. return;

  21. }

  22. printInOrder(head.right, height + 1, "v", len);

  23. String val = to + head.value + to;

  24. int lenM = val.length();

  25. int lenL = (len - lenM) / 2;

  26. int lenR = len - lenM - lenL;

  27. val = getSpace(lenL) + val + getSpace(lenR);

  28. System.out.println(getSpace(height * len) + val);

  29. printInOrder(head.left, height + 1, "^", len);

  30. }

  31.  
  32. public static String getSpace(int num) {

  33. String space = " ";

  34. StringBuffer buf = new StringBuffer("");

  35. for (int i = 0; i < num; i++) {

  36. buf.append(space);

  37. }

  38. return buf.toString();

  39. }

  40.  
  41. public static void main(String[] args) {

  42. Node head = new Node(4);

  43. head.left = new Node(2);

  44. head.right = new Node(6);

  45. head.left.left = new Node(1);

  46. head.left.right = new Node(3);

  47. head.right.left = new Node(5);

  48. head.right.right = new Node(7);

  49. printTree(head);

  50.  
  51. }

  52.  
  53. }

判断平衡二叉树

平衡二叉树(Balanced Binary Tree)具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。并且左右两个子树都是一棵平衡二叉树

(不是我们平时意义上的必须为搜索树)

判断一棵树是否为平衡二叉树:

 

可以暴力判断:每一颗树是否为平衡二叉树。

 

分析:

如果左右子树都已知是平衡二叉树,而左子树和右子树高度差绝对值不超过1,本树就是平衡的。

 

为此我们需要的信息:左右子树是否为平衡二叉树。左右子树的高度。

 

我们需要给父返回的信息就是:本棵树是否是平衡的、本棵树的高度。

 

定义结点和返回值:

 
  1. public static class Node {

  2. public int value;

  3. public Node left;

  4. public Node right;

  5.  
  6. public Node(int data) {

  7. this.value = data;

  8. }

  9. }

 
  1. public static class ReturnType {

  2. public int level; //深度

  3. public boolean isB;//本树是否平衡

  4.  
  5. public ReturnType(int l, boolean is) {

  6. level = l;

  7. isB = is;

  8. }

  9. }

我们把代码写出来:

 
  1. // process(head, 1)

  2.  
  3. public static ReturnType process(Node head, int level) {

  4. if (head == null) {

  5. return new ReturnType(level, true);

  6. }

  7. //取信息

  8. ReturnType leftSubTreeInfo = process(head.left, level + 1);

  9. if(!leftSubTreeInfo.isB) {

  10. return new ReturnType(level, false); //左子树不是->返回

  11. }

  12. ReturnType rightSubTreeInfo = process(head.right, level + 1);

  13. if(!rightSubTreeInfo.isB) {

  14. return new ReturnType(level, false); //右子树不是->返回

  15. }

  16. if (Math.abs(rightSubTreeInfo.level - leftSubTreeInfo.level) > 1) {

  17. return new ReturnType(level, false); //左右高度差大于1->返回

  18. }

  19.  
  20. return new ReturnType(Math.max(leftSubTreeInfo.level, rightSubTreeInfo.level), true);

  21. //返回高度和true(当前树是平衡的)

  22. }

我们不需要每次都返回高度,用一个全局变量记录即可。

对于其它二叉树问题,可能不止一个变量信息,所以,全局记录最好都养成定义数组的习惯。

下面贴出完整代码:

 
  1. import java.util.LinkedList;

  2. import java.util.Queue;

  3.  
  4. public class Demo {

  5. public static class Node {

  6. public int value;

  7. public Node left;

  8. public Node right;

  9.  
  10. public Node(int data) {

  11. this.value = data;

  12. }

  13. }

  14. public static boolean isBalance(Node head) {

  15. boolean[] res = new boolean[1];

  16. res[0] = true;

  17. getHeight(head, 1, res);

  18. return res[0];

  19. }

  20.  
  21. public static class ReturnType {

  22. public int level; //深度

  23. public boolean isB;//本树是否平衡

  24.  
  25. public ReturnType(int l, boolean is) {

  26. level = l;

  27. isB = is;

  28. }

  29. }

  30.  
  31. // process(head, 1)

  32.  
  33. public static ReturnType process(Node head, int level) {

  34. if (head == null) {

  35. return new ReturnType(level, true);

  36. }

  37. //取信息

  38. ReturnType leftSubTreeInfo = process(head.left, level + 1);

  39. if(!leftSubTreeInfo.isB) {

  40. return new ReturnType(level, false); //左子树不是->返回

  41. }

  42. ReturnType rightSubTreeInfo = process(head.right, level + 1);

  43. if(!rightSubTreeInfo.isB) {

  44. return new ReturnType(level, false); //右子树不是->返回

  45. }

  46. if (Math.abs(rightSubTreeInfo.level - leftSubTreeInfo.level) > 1) {

  47. return new ReturnType(level, false); //左右高度差大于1->返回

  48. }

  49.  
  50. return new ReturnType(Math.max(leftSubTreeInfo.level, rightSubTreeInfo.level), true);

  51. //返回高度和true(当前树是平衡的

  52. }

  53.  
  54. public static int getHeight(Node head, int level, boolean[] res) {

  55. if (head == null) {

  56. return level;//返回高度

  57. }

  58. //取信息

  59. //相同逻辑

  60. int lH = getHeight(head.left, level + 1, res);

  61. if (!res[0]) {

  62. return level;

  63. }

  64. int rH = getHeight(head.right, level + 1, res);

  65. if (!res[0]) {

  66. return level;

  67. }

  68. if (Math.abs(lH - rH) > 1) {

  69. res[0] = false;

  70. }

  71. return Math.max(lH, rH);//返回高度

  72. }

  73.  
  74. public static void main(String[] args) {

  75. Node head = new Node(1);

  76. head.left = new Node(2);

  77. head.right = new Node(3);

  78. head.left.left = new Node(4);

  79. head.left.right = new Node(5);

  80. head.right.left = new Node(6);

  81. head.right.right = new Node(7);

  82.  
  83. System.out.println(isBalance(head));

  84.  
  85. }

  86.  
  87. }

判断完全二叉树

完全二叉树的定义: 一棵二叉树,除了最后一层之外都是完全填充的,并且最后一层的叶子结点都在左边。

https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91/7773232?fr=aladdin

百度定义

 

思路:层序遍历二叉树

如果一个结点,左右孩子都不为空,则pop该节点,将其左右孩子入队列

如果一个结点,左孩子为空,右孩子不为空,则该树一定不是完全二叉树

如果一个结点,左孩子不为空,右孩子为空;或者左右孩子都为空:::::则该节点之后的队列中的结点都为叶子节点;该树才是完全二叉树,否则返回false。

非完全二叉树的例子(对应方法的正确性和必要性):

下面写代码:

定义结点:

 
  1. public static class Node {

  2. public int value;

  3. public Node left;

  4. public Node right;

  5.  
  6. public Node(int data) {

  7. this.value = data;

  8. }

  9. }

方法:

 
  1. public static boolean isCBT(Node head) {

  2. if (head == null) {

  3. return true;

  4. }

  5. Queue<Node> queue = new LinkedList<Node>();

  6. boolean leaf = false;

  7. Node l = null;

  8. Node r = null;

  9. queue.offer(head);

  10. while (!queue.isEmpty()) {

  11. head = queue.poll();

  12. l = head.left;

  13. r = head.right;

  14. if ((leaf && (l != null || r != null)) || (l == null && r != null)) {

  15. return false;//当前结点不是叶子结点且之前结点有叶子结点 || 当前结点有右孩子无左孩子

  16. }

  17. if (l != null) {

  18. queue.offer(l);

  19. }

  20. if (r != null) {

  21. queue.offer(r);

  22. } else {

  23. leaf = true;//无孩子即为叶子结点

  24. }

  25. }

  26. return true;

  27. }

判断二叉搜索树

二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树

 

判断某棵树是否为二叉搜索树

 

单纯判断每个结点比左孩子大比右孩子小是不对的。如图:

15推翻了这种方法。

 

思路:

1)可以根据定义判断,递归进行,如果左右子树都为搜索二叉树,且左子树最大值小于根,右子树最小值大于根。成立。

2)根据定义,中序遍历为递增序列,我们中序遍历后判断是否递增即可。

3)我们可以在中序遍历过程中判断之前节点和当前结点的关系,不符合直接返回false即可。

4)进一步通过morris遍历优化

morris遍历:https://blog.csdn.net/hebtu666/article/details/83093983

 

 
  1. public static class Node {

  2. public int value;

  3. public Node left;

  4. public Node right;

  5.  
  6. public Node(int data) {

  7. this.value = data;

  8. }

  9. }

  10. public static boolean isBST(Node head) {

  11. if (head == null) {

  12. return true;

  13. }

  14. boolean res = true;

  15. Node pre = null;

  16. Node cur1 = head;

  17. Node cur2 = null;

  18. while (cur1 != null) {

  19. cur2 = cur1.left;

  20. if (cur2 != null) {

  21. while (cur2.right != null && cur2.right != cur1) {

  22. cur2 = cur2.right;

  23. }

  24. if (cur2.right == null) {

  25. cur2.right = cur1;

  26. cur1 = cur1.left;

  27. continue;

  28. } else {

  29. cur2.right = null;

  30. }

  31. }

  32. if (pre != null && pre.value > cur1.value) {

  33. res = false;

  34. }

  35. pre = cur1;

  36. cur1 = cur1.right;

  37. }

  38. return res;

  39. }

二叉搜索树实现

本文给出二叉搜索树介绍和实现

 

首先说它的性质:所有的节点都满足,左子树上所有的节点都比自己小,右边的都比自己大。

 

那这个结构有什么有用呢?

首先可以快速二分查找。还可以中序遍历得到升序序列,等等。。。

基本操作:

1、插入某个数值

2、查询是否包含某个数值

3、删除某个数值

 

根据实现不同,还可以实现其他很多种操作。

 

实现思路思路:

前两个操作很好想,就是不断比较,大了往左走,小了往右走。到空了插入,或者到空都没找到。

而删除稍微复杂一些,有下面这几种情况:

1、需要删除的节点没有左儿子,那就把右儿子提上去就好了。

2、需要删除的节点有左儿子,这个左儿子没有右儿子,那么就把左儿子提上去

3、以上都不满足,就把左儿子子孙中最大节点提上来。

 

当然,反过来也是成立的,比如右儿子子孙中最小的节点。

 

下面来叙述为什么可以这么做。

下图中A为待删除节点。

第一种情况:

 

1、去掉A,把c提上来,c也是小于x的没问题。

2、根据定义可知,x左边的所有点都小于它,把c提上来不影响规则。

 

第二种情况

 

3、B<A<C,所以B<C,根据刚才的叙述,B可以提上去,c可以放在b右边,不影响规则

4、同理

 

第三种情况

 

5、注意:是把黑色的提升上来,不是所谓的最右边的那个,因为当初向左拐了,他一定小。

因为黑色是最大,比B以及B所有的孩子都大,所以让B当左孩子没问题

而黑点小于A,也就小于c,所以可以让c当右孩子

大概证明就这样。。

下面我们用代码实现并通过注释理解

上次链表之类的用的c,循环来写的。这次就c++函数递归吧,不同方式练习。

定义

 
  1. struct node

  2. {

  3. int val;//数据

  4. node *lch,*rch;//左右孩子

  5. };

插入

 
  1. node *insert(node *p,int x)

  2. {

  3. if(p==NULL)//直到空就创建节点

  4. {

  5. node *q=new node;

  6. q->val=x;

  7. q->lch=q->rch=NULL;

  8. return p;

  9. }

  10. if(x<p->val)p->lch=insert(p->lch,x);

  11. else p->lch=insert(p->rch,x);

  12. return p;//依次返回自己,让上一个函数执行。

  13. }

查找

 
  1. bool find(node *p,int x)

  2. {

  3. if(p==NULL)return false;

  4. else if(x==p->val)return true;

  5. else if(x<p->val)return find(p->lch,x);

  6. else return find(p->rch,x);

  7. }

删除

 
  1. node *remove(node *p,int x)

  2. {

  3. if(p==NULL)return NULL;

  4. else if(x<p->val)p->lch=remove(p->lch,x);

  5. else if(x>p->val)p->lch=remove(p->rch,x);

  6. //以下为找到了之后

  7. else if(p->lch==NULL)//情况1

  8. {

  9. node *q=p->rch;

  10. delete p;

  11. return q;

  12. }

  13. else if(p->lch->rch)//情况2

  14. {

  15. node *q=p->lch;

  16. q->rch=p->rch;

  17. delete p;

  18. return q;

  19. }

  20. else

  21. {

  22. node *q;

  23. for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大节点的前一个

  24. node *r=q->rch;//最大节点

  25. q->rch=r->lch;//最大节点左孩子提到最大节点位置

  26. r->lch=p->lch;//调整黑点左孩子为B

  27. r->rch=p->rch;//调整黑点右孩子为c

  28. delete p;//删除

  29. return r;//返回给父

  30. }

  31. return p;

  32. }

堆的简单实现

关于堆不做过多介绍

堆就是儿子的值一定不小于父亲的值并且树的节点都是按照从上到下,从左到右紧凑排列的树。

(本文为二叉堆)

具体实现并不需要指针二叉树,用数组储存并且利用公式找到父子即可。

父:(i-1)/2

子:i*2+1,i*2+2

插入:首先把新数字放到堆的末尾,也就是右下角,然后查看父的数值,需要交换就交换,重复上述操作直到不需交换

删除:把堆的第一个节点赋值为最后一个节点的值,然后删除最后一个节点,不断向下交换。

(两个儿子:严格来说要选择数值较小的那一个)

时间复杂度:和深度成正比,所以n个节点是O(logN)

 
  1. int heap[MAX_N],sz=0;

  2. //定义数组和记录个数的变量

插入代码:

 
  1. void push(int x)

  2. {//节点编号

  3. int i=sz++;

  4. while(i>0)

  5. {

  6. int p=(i-1)/2;//父

  7. if(heap[p]<=x)break;//直到大小顺序正确跳出循环

  8. heap[i]=heap[p];//把父节点放下来

  9. i=p;

  10. }

  11. heap[i]=x;//最后把自己放上去

  12.  
  13. }

弹出:

 
  1. int pop()

  2. {

  3. int ret=heap[0];//保存好值,最后返回

  4. int x=heap[--sz];

  5. while(i*2+1<sz)

  6. {

  7. int a=i*2+1;//左孩子

  8. int b=i*2+2;//右孩子

  9. if(b<sz && heap[b]<heap[a])a=b;//找最小

  10. if(heap[a]>=x)break;//直到不需要交换就退出

  11. heap[i]=heap[a];//把儿子放上来

  12. i=a;

  13. }

  14. head[i]=x;//下沉到正确位置

  15. return ret;//返回

  16. }

堆应用例题三连

一个数据流中,随时可以取得中位数。


题目描述:有一个源源不断地吐出整数的数据流,假设你有足够的空间来保存吐出的数。请设计一个名叫MedianHolder的结构,MedianHolder可以随时取得之前吐出所有树的中位数。

要求:

1.如果MedianHolder已经保存了吐出的N个数,那么任意时刻将一个新的数加入到MedianHolder的过程中,时间复杂度O(logN)。

2.取得已经吐出的N个数整体的中位数的过程,时间复杂度O(1).

 

看这要求就应该感觉到和堆相关吧?

但是进一步没那么好想。

设计的MedianHolder中有两个堆,一个是大根堆,一个是小根堆。大根堆中含有接收的所有数中较小的一半,并且按大根堆的方式组织起来,那么这个堆的堆顶就是较小一半的数中最大的那个。小根堆中含有接收的所有数中较大的一半,并且按小根堆的方式组织起来,那么这个堆的堆顶就是较大一半的数中最小的那个。

例如,如果已经吐出的数为6,1,3,0,9,8,7,2.

较小的一半为:0,1,2,3,那么3就是这一半的数组成的大根堆的堆顶

较大的一半为:6,7,8,9,那么6就是这一半的数组成的小根堆的堆顶

因为此时数的总个数为偶数,所以中位数就是两个堆顶相加,再除以2.

如果此时新加入一个数10,那么这个数应该放进较大的一半里,所以此时较大的一半数为6,7,8,9,10,此时6依然是这一半的数组成的小根堆的堆顶,因为此时数的总个数为奇数,所以中位数应该是正好处在中间位置的数,而此时大根堆有4个数,小根堆有5个数,那么小根堆的堆顶6就是此时的中位数。

如果此时又新加入一个数11,那么这个数也应该放进较大的一半里,此时较大一半的数为:6,7,8,9,10,11.这个小根堆大小为6,而大根堆的大小为4,所以要进行如下调整:

1.如果大根堆的size比小根堆的size大2,那么从大根堆里将堆顶元素弹出,并放入小根堆里

2,如果小根堆的size比大根堆的size大2,那么从小根堆里将堆顶弹出,并放入大根堆里。

经过这样的调整之后,大根堆和小根堆的size相同。

总结如下:

大根堆每时每刻都是较小的一半的数,堆顶为这一堆数的最大值
小根堆每时每刻都是较大的一半的数,堆顶为这一堆数的最小值
新加入的数根据与两个堆堆顶的大小关系,选择放进大根堆或者小根堆里(或者放进任意一个堆里)
当任何一个堆的size比另一个size大2时,进行如上调整的过程。


这样随时都可以知道已经吐出的所有数处于中间位置的两个数是什么,取得中位数的操作时间复杂度为O(1),同时根据堆的性质,向堆中加一个新的数,并且调整堆的代价为O(logN)。
 

 
  1. import java.util.Arrays;

  2. import java.util.Comparator;

  3. import java.util.PriorityQueue;

  4.  
  5. /**

  6. * 随时找到数据流的中位数

  7. * 思路:

  8. * 利用一个大根堆和一个小根堆去保存数据,保证前一半的数放在大根堆,后一半的数放在小根堆

  9. * 在添加数据的时候,不断地调整两个堆的大小,使得两个堆保持平衡

  10. * 要取得的中位数就是两个堆堆顶的元素

  11. */

  12. public class MedianQuick {

  13. public static class MedianHolder {

  14. private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(new MaxHeapComparator());

  15. private PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>(new MinHeapComparator());

  16.  
  17. /**

  18. * 调整堆的大小

  19. * 当两个堆的大小差值变大时,从数据多的堆中弹出一个数据进入另一个堆中

  20. */

  21. private void modifyTwoHeapsSize() {

  22. if (this.maxHeap.size() == this.minHeap.size() + 2) {

  23. this.minHeap.add(this.maxHeap.poll());

  24. }

  25. if (this.minHeap.size() == this.maxHeap.size() + 2) {

  26. this.maxHeap.add(this.minHeap.poll());

  27. }

  28. }

  29.  
  30. /**

  31. * 添加数据的过程

  32. *

  33. * @param num

  34. */

  35. public void addNumber(int num) {

  36. if (this.maxHeap.isEmpty()) {

  37. this.maxHeap.add(num);

  38. return;

  39. }

  40. if (this.maxHeap.peek() >= num) {

  41. this.maxHeap.add(num);

  42. } else {

  43. if (this.minHeap.isEmpty()) {

  44. this.minHeap.add(num);

  45. return;

  46. }

  47. if (this.minHeap.peek() > num) {

  48. this.maxHeap.add(num);

  49. } else {

  50. this.minHeap.add(num);

  51. }

  52. }

  53. modifyTwoHeapsSize();

  54. }

  55.  
  56. /**

  57. * 获取中位数

  58. *

  59. * @return

  60. */

  61. public Integer getMedian() {

  62. int maxHeapSize = this.maxHeap.size();

  63. int minHeapSize = this.minHeap.size();

  64. if (maxHeapSize + minHeapSize == 0) {

  65. return null;

  66. }

  67. Integer maxHeapHead = this.maxHeap.peek();

  68. Integer minHeapHead = this.minHeap.peek();

  69. if (((maxHeapSize + minHeapSize) & 1) == 0) {

  70. return (maxHeapHead + minHeapHead) / 2;

  71. }

  72. return maxHeapSize > minHeapSize ? maxHeapHead : minHeapHead;

  73. }

  74. }

  75.  
  76. /**

  77. * 大根堆比较器

  78. */

  79. public static class MaxHeapComparator implements Comparator<Integer> {

  80. @Override

  81. public int compare(Integer o1, Integer o2) {

  82. if (o2 > o1) {

  83. return 1;

  84. } else {

  85. return -1;

  86. }

  87. }

  88. }

  89.  
  90. /**

  91. * 小根堆比较器

  92. */

  93. public static class MinHeapComparator implements Comparator<Integer> {

  94. @Override

  95. public int compare(Integer o1, Integer o2) {

  96. if (o2 < o1) {

  97. return 1;

  98. } else {

  99. return -1;

  100. }

  101. }

  102. }

  103.  
  104. // for test

  105. public static int[] getRandomArray(int maxLen, int maxValue) {

  106. int[] res = new int[(int) (Math.random() * maxLen) + 1];

  107. for (int i = 0; i != res.length; i++) {

  108. res[i] = (int) (Math.random() * maxValue);

  109. }

  110. return res;

  111. }

  112.  
  113. // for test, this method is ineffective but absolutely right

  114. public static int getMedianOfArray(int[] arr) {

  115. int[] newArr = Arrays.copyOf(arr, arr.length);

  116. Arrays.sort(newArr);

  117. int mid = (newArr.length - 1) / 2;

  118. if ((newArr.length & 1) == 0) {

  119. return (newArr[mid] + newArr[mid + 1]) / 2;

  120. } else {

  121. return newArr[mid];

  122. }

  123. }

  124.  
  125. public static void printArray(int[] arr) {

  126. for (int i = 0; i != arr.length; i++) {

  127. System.out.print(arr[i] + " ");

  128. }

  129. System.out.println();

  130. }

  131.  
  132. public static void main(String[] args) {

  133. boolean err = false;

  134. int testTimes = 200000;

  135. for (int i = 0; i != testTimes; i++) {

  136. int len = 30;

  137. int maxValue = 1000;

  138. int[] arr = getRandomArray(len, maxValue);

  139. MedianHolder medianHold = new MedianHolder();

  140. for (int j = 0; j != arr.length; j++) {

  141. medianHold.addNumber(arr[j]);

  142. }

  143. if (medianHold.getMedian() != getMedianOfArray(arr)) {

  144. err = true;

  145. printArray(arr);

  146. break;

  147. }

  148. }

  149. System.out.println(err ? "Oops..what a fuck!" : "today is a beautiful day^_^");

  150.  
  151. }

  152. }

金条

 

一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金条,不管切成长度多大的两半,都要花费20个铜板。一群人想整分整块金条,怎么分最省铜板?
例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60,金条要分成10,20,30三个部分。如果,先把长度60的金条分成10和50,花费60,再把长度为50的金条分成20和30,花费50,一共花费110个铜板。

但是如果,先把长度60的金条分成30和30,花费60,再把长度30金条分成10和30,花费30,一共花费90个铜板。

输入一个数组,返回分割的最小代价。

首先我们要明白一点:不管合并策略是什么我们一共会合并n-1次,这个次数是不会变的。

我们要做的就是每一次都做最优选择。

合为最优?

最小的两个数合并就是最优。

所以

1)首先构造小根堆

2)每次取最小的两个数(小根堆),使其代价最小。并将其和加入到小根堆中

3)重复(2)过程,直到最后堆中只剩下一个节点。

 

花费为每次花费的累加。

代码略。

 

项目最大收益(贪心问题)


输入:参数1,正数数组costs,参数2,正数数组profits,参数3,正数k,参数4,正数m

costs[i]表示i号项目的花费profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润),k表示你不能并行,只能串行的最多做k个项目,m表示你初始的资金。

说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个项目。

输出:你最后获得的最大钱数。

思考:给定一个初始化投资资金,给定N个项目,想要获得其中最大的收益,并且一次只能做一个项目。这是一个贪心策略的问题,应该在能做的项目中选择收益最大的。

按照花费的多少放到一个小根堆里面,然后要是小根堆里面的头节点的花费少于给定资金,就将头节点一个个取出来,放到按照收益的大根堆里面。然后做大根堆顶的项目即可。

 并查集实现

并查集是什么东西?

它是用来管理元素分组情况的一种数据结构。

他可以高效进行两个操作:

  1. 查询a,b是否在同一组
  2. 合并a和b所在的组

萌新可能不知所云,这个结构到底有什么用?

经分析,并查集效率之高超乎想象,对n个元素的并查集进行一次操作的复杂度低于O(logn)

 

我们先说并查集是如何实现的:

也是使用树形结构,但不是二叉树。

每个元素就是一个结点,每组都是一个树。

无需关注它的形状,或哪个节点具体在哪个位置。

 

初始化:

我们现在有n个结点,也就是n个元素。

 

合并:

然后我们就可以合并了,合并方法就是把一个根放到另一颗树的下面,也就是整棵树作为人家的一个子树。

 

查询:

查询两个结点是否是同一组,需要知道这两个结点是不是在一棵树上,让他们分别沿着树向根找,如果两个元素最后走到一个根,他们就在一组。

 

当然,树形结构都存在退化的缺点,对于每种结构,我们都有自己的优化方法,下面我们说明如何避免退化。

  1. 记录每一棵树的高度,合并操作时,高度小的变为高度大的子树即可。
  2. 路径压缩:对于一个节点,只要走到了根节点,就不必再在很深的地方,直接改为连着根即可。进一步优化:其实每一个经过的节点都可以直接连根。

这样查询的时候就能很快地知道根是谁了。

 

下面上代码实现:

和很多树结构一样,我们没必要真的模拟出来,数组中即可。

 
  1. int p[MAX_N];//父亲

  2. int rank[MAX_N];//高度

  3. //初始化

  4. void gg(int n)

  5. {

  6. for(int i=0;i<n;i++)

  7. {

  8. p[i]=i;//父是自己代表是根

  9. rank[i]=0;

  10. }

  11. }

  12. //查询根

  13. int find(int x)

  14. {

  15. if(p[x]==x)return x;

  16. return p[x]=find(p[x])//不断把经过的结点连在根

  17. }

  18. //判断是否属于同一组

  19. bool judge(int x,int y)

  20. {

  21. return find(x)==find(y);//查询结果一样就在一组

  22. }

  23. //合并

  24. void unite(int x,int y)

  25. {

  26. if(x==y)return;

  27. if(rank[x]<rank[y])p[x]=y;//深度小,放在大的下面

  28. else

  29. {

  30. p[y]=x;

  31. if(rank[x]=rank[y])rank[x]++;//一样,y放x后,x深度加一

  32. }

  33. }

实现很简单,应用有难度,以后有时间更新题。

并查集入门三连:HDU1213 POJ1611 POJ2236

HDU1213

http://acm.hdu.edu.cn/showproblem.php?pid=1213

问题描述

今天是伊格纳修斯的生日。他邀请了很多朋友。现在是晚餐时间。伊格纳修斯想知道他至少需要多少桌子。你必须注意到并非所有的朋友都互相认识,而且所有的朋友都不想和陌生人呆在一起。

这个问题的一个重要规则是,如果我告诉你A知道B,B知道C,那意味着A,B,C彼此了解,所以他们可以留在一个表中。

例如:如果我告诉你A知道B,B知道C,D知道E,所以A,B,C可以留在一个表中,D,E必须留在另一个表中。所以Ignatius至少需要2张桌子。

输入

输入以整数T(1 <= T <= 25)开始,表示测试用例的数量。然后是T测试案例。每个测试用例以两个整数N和M开始(1 <= N,M <= 1000)。N表示朋友的数量,朋友从1到N标记。然后M行跟随。每一行由两个整数A和B(A!= B)组成,这意味着朋友A和朋友B彼此了解。两个案例之间会有一个空白行。

 

对于每个测试用例,只输出Ignatius至少需要多少个表。不要打印任何空白。

样本输入

2

5 3

1 2

2 3

4 5

 

5 1

2 5

样本输出

2

4

并查集基础题

 
  1. #include<cstdio>

  2. #include<iostream>

  3. using namespace std;

  4. int fa[1005];

  5. int n,m;

  6. void init()//初始化

  7. {

  8. for(int i=0;i<1005;i++)

  9. fa[i]=i;

  10. }

  11. int find(int x)//寻根

  12. {

  13. if(fa[x]!=x)

  14. fa[x]=find(fa[x]);

  15. return fa[x];

  16. }

  17. void union(int x,int y)//判断、合并

  18. {

  19. int a=find(x),b=find(y);

  20. if(a!=b)

  21. fa[b]=a;

  22. }

  23. int main()

  24. {

  25. int t;

  26. scanf("%d",&t);

  27. while(t--)

  28. {

  29. int a,b,cnt=0;

  30. scanf("%d%d",&n,&m);

  31. init();

  32. for(int i=1;i<=m;i++)//合并

  33. {

  34. scanf("%d%d",&a,&b);

  35. union(a,b);

  36. }

  37. for(int i=1;i<=n;i++)//统计

  38. {

  39. find(i);

  40. if(find(i)==i)

  41. cnt++;

  42. }

  43. printf("%d\n",cnt);

  44. }

  45. return 0;

  46. }

POJ1611

http://poj.org/problem?id=1611

描述

严重急性呼吸系统综合症(SARS)是一种病因不明的非典型肺炎,在2003年3月中旬被认为是一种全球性威胁。为了尽量减少对他人的传播,最好的策略是将嫌疑人与其他嫌疑人分开。 
在Not-Spreading-Your-Sickness University(NSYSU),有许多学生团体。同一组中的学生经常互相交流,学生可以加入几个小组。为了防止可能的SARS传播,NSYSU收集所有学生组的成员列表,并在其标准操作程序(SOP)中制定以下规则。 
一旦组中的成员是嫌疑人,该组中的所有成员都是嫌疑人。 
然而,他们发现,当学生被认定为嫌疑人时,识别所有嫌疑人并不容易。你的工作是编写一个找到所有嫌疑人的程序。

输入

输入文件包含几种情况。每个测试用例以一行中的两个整数n和m开始,其中n是学生数,m是组的数量。您可以假设0 <n <= 30000且0 <= m <= 500.每个学生都使用0到n-1之间的唯一整数进行编号,并且最初学生0在所有情况下都被识别为嫌疑人。该行后面是组的m个成员列表,每组一行。每行以整数k开头,表示组中的成员数。在成员数量之后,有k个整数代表该组中的学生。一行中的所有整数由至少一个空格分隔。 


n = 0且m = 0的情况表示输入结束,无需处理。

 

对于每种情况,输出一行中的嫌疑人数量。

样本输入

 
  1. 100 4

  2. 2 1 2

  3. 5 10 13 11 12 14

  4. 2 0 1

  5. 2 99 2

  6. 200 2

  7. 1 5

  8. 5 1 2 3 4 5

  9. 1 0

  10. 0 0

样本输出

 
  1. 4

  2. 1

  3. 1

 

 
  1. #include<iostream>

  2. #include<cstdio>

  3. #include<algorithm>

  4. #include<cstring>

  5. #include <string>

  6. using namespace std;

  7. int a[30001],pre[30001];

  8. int find(int x)//寻根

  9. {

  10.  if(pre[x]==x)

  11.         return x;

  12.     else

  13.         return pre[x]=find(pre[x]);

  14. }

  15. void union(int x, int y)//合并

  16. {

  17. int fx = find(x), fy = find(y);

  18. if (fx != fy)

  19. pre[fy] = fx;

  20. }

  21.  
  22. int main()

  23. {

  24. int n,m;

  25. while (scanf("%d%d", &n, &m) != EOF && (n || m))

  26. {

  27. int sum = 0;

  28. for (int i = 0; i < n; i++)//初始化

  29. pre[i] = i;

  30. for (int i = 0; i < m; i++)

  31. {

  32. int k;

  33. scanf("%d", &k);

  34. if (k >= 1)

  35. {

  36. scanf("%d", &a[0]);

  37. for (int j = 1; j < k; j++)

  38. {

  39. scanf("%d", &a[j]);//接收

  40. union(a[0], a[j]);//和0号一组

  41. }

  42. }

  43. }

  44. for (int i = 0; i < n; i++)//统计

  45. if (find(i) ==pre[0])

  46. sum++;

  47. printf("%d\n", sum);

  48. }

  49. return 0;

  50. }

 POJ2236

http://poj.org/problem?id=2236

描述

地震发生在东南亚。ACM(亚洲合作医疗团队)已经与膝上电脑建立了无线网络,但是一次意外的余震袭击,网络中的所有计算机都被打破了。计算机一个接一个地修复,网络逐渐开始工作。由于硬件限制,每台计算机只能直接与距离它不远的计算机进行通信。但是,每台计算机都可以被视为两台计算机之间通信的中介,也就是说,如果计算机A和计算机B可以直接通信,或者计算机C可以与A和A进行通信,则计算机A和计算机B可以进行通信。 B. 

在修复网络的过程中,工作人员可以随时进行两种操作,修复计算机或测试两台计算机是否可以通信。你的工作是回答所有的测试操作。 

输入

第一行包含两个整数N和d(1 <= N <= 1001,0 <= d <= 20000)。这里N是计算机的数量,编号从1到N,D是两台计算机可以直接通信的最大距离。在接下来的N行中,每行包含两个整数xi,yi(0 <= xi,yi <= 10000),这是N台计算机的坐标。从第(N + 1)行到输入结束,有一些操作,这些操作是一个接一个地执行的。每行包含以下两种格式之一的操作: 
1。“O p”(1 <= p <= N),表示修复计算机p。 
2.“S p q”(1 <= p,q <= N),这意味着测试计算机p和q是否可以通信。 

输入不会超过300000行。 

产量

对于每个测试操作,如果两台计算机可以通信则打印“SUCCESS”,否则打印“FAIL”。

样本输入

 
  1. 4 1

  2. 0 1

  3. 0 2

  4. 0 3

  5. 0 4

  6. O 1

  7. O 2

  8. O 4

  9. S 1 4

  10. O 3

  11. S 1 4

样本输出

 
  1. FAIL

  2. SUCCESS

 思路:对每次修好的电脑对其它已经修好的电脑遍历,如果距离小于等于最大通信距离就将他们合并。

注意

  1、坐标之后给出的计算机编号都是n+1的。例如O 3,他实际上修理的是编号为2的计算机,因为计算机是从0开始编号的。

  2、比较距离的时候注意要用浮点数比较,否则会WA。

  3、"FAIL"不要写成"FALL"。

  4、字符串输入的时候注意处理好回车,空格等情况。

  5、注意N的范围(1 <= N <= 1001),最大是1001,不是1000。是个小坑,数组开小了可能会错哦。

 

 
  1. #include <iostream>

  2. #include <stdio.h>

  3. #include <cmath>

  4. using namespace std;

  5.  
  6. #define MAXN 1010

  7.  
  8. int dx[MAXN],dy[MAXN]; //坐标

  9. int par[MAXN]; //x的父节点

  10. int repair[MAXN] ={0};

  11. int n;

  12.  
  13. void Init()//初始化

  14. {

  15. int i;

  16. for(i=0;i<=n;i++)

  17. par[i] = i;

  18. }

  19.  
  20. int Find(int x)//寻根

  21. {

  22. if(par[x]!=x)

  23. par[x] = Find(par[x]);

  24. return par[x];

  25. }

  26.  
  27. void Union(int x,int y)//合并

  28. {

  29. par[Find(x)] = Find(y);

  30. }

  31.  
  32. int Abs(int n)//绝对值

  33. {

  34. return n>0?n:-n;

  35. }

  36.  
  37. double Dis(int a,int b)//坐标

  38. {

  39. return sqrt( double(dx[a]-dx[b])*(dx[a]-dx[b]) + (dy[a]-dy[b])*(dy[a]-dy[b]) );

  40. }

  41.  
  42. int main()

  43. {

  44. int d,i;

  45.  
  46. //初始化

  47. scanf("%d%d",&n,&d);

  48. Init();

  49.  
  50. //输入坐标

  51. for(i=0;i<n;i++){

  52. scanf("%d%d",&dx[i],&dy[i]);

  53. }

  54.  
  55. //操作

  56. char cmd[2];

  57. int p,q,len=0;

  58. while(scanf("%s",cmd)!=EOF)

  59. {

  60. switch(cmd[0])

  61. {

  62. case 'O':

  63. scanf("%d",&p);

  64. p--;

  65. repair[len++] = p;

  66. for(i=0;i<len-1;i++) //遍历所有修过的计算机,看能否联通

  67. if( repair[i]!=p && Dis(repair[i],p)<=double(d) )

  68. Union(repair[i],p);

  69. break;

  70. case 'S':

  71. scanf("%d%d",&p,&q);

  72. p--,q--;

  73. if(Find(p)==Find(q)) //判断

  74. printf("SUCCESS\n");

  75. else

  76. printf("FAIL\n");

  77. default:

  78. break;

  79. }

  80. }

  81.  
  82. return 0;

  83. }

线段树简单实现

首先,线段树是一棵满二叉树。(每个节点要么有两个孩子,要么是深度相同的叶子节点)

每个节点维护某个区间,根维护所有的。

 转存失败重新上传取消 

如图,区间是二分父的区间。

当有n个元素,初始化需要o(n)时间,对区间操作需要o(logn)时间。

下面给出维护区间最小值的思路和代码

功能:一样的,依旧是查询和改值。

查询[s,t]之间最小的数。修改某个值。

 

从下往上,每个节点的值为左右区间较小的那一个即可。

这算是简单动态规划思想,做到了o(n),因为每个节点就访问一遍,而叶子节点一共n个,所以访问2n次即可。

如果利用深搜初始化,会到o(nlogn)。

https://blog.csdn.net/hebtu666/article/details/81777273

有介绍

那我们继续说,如何查询。

不要以为它是二分区间就只能查二分的那些区间,它能查任意区间。

比如上图,求1-7的最小值,查询1-4,5-6,7-7即可。

下面说过程:

递归实现:

如果要查询的区间和本节点区间没有重合,返回一个特别大的数即可,不要影响其他结果。

如果要查询的区间完全包含了本节点区间,返回自身的值

都不满足,对左右儿子做递归,返回较小的值。

 

如何更新?

更新ai,就要更新所有包含ai的区间。

可以从下往上不断更新,把节点的值更新为左右孩子较小的即可。

 

代码实现和相关注释:

注:没有具体的初始化,dp思路写过了,实在不想写了

初始全为INT_MAX

 
  1. const int MAX_N=1<<7;

  2. int n;

  3. int tree[2*MAX_N-1];

  4. //初始化

  5. void gg(int nn)

  6. {

  7. n=1;

  8. while(n<nn)n*=2;//把元素个数变为2的n次方

  9. for(int i=0;i<2*n-1;i++)tree[i]=INTMAX;//所有值初始化为INTMAX

  10. }

  11.  
  12. //查询区间最小值

  13. int get(int a,int b,int k,int l,int r)//l和r是区间,k是节点下标,求[a,b)最小值

  14. {

  15. if(a>=r || b<=l)return INTMAX;//情况1

  16. if(a<=l || b<=b)return tree[k];//情况2

  17. int ll=get(a,b,k*2+1,l,(l+r)/2);//以前写过,左孩子公式

  18. int rr=get(a,b,k*2+2,(l+r)/2,r);//右孩子

  19. return min(ll,rr);

  20. }

  21.  
  22. //更新

  23. void update(int k,int a)//第k个值更新为a

  24. {

  25. //本身

  26. k+=n-1;//加上前面一堆节点数

  27. tree[k]=a;

  28. //开始向上

  29. while(k>0)

  30. {

  31. tree[k]=min(tree[2*k+1],tree[2*k+2]);

  32. k=(k-1)/2//父的公式,也写过

  33. }

  34. }

 树状数组实现

树状数组能够完成如下操作:

给一个序列a0-an

计算前i项和

对某个值加x

时间o(logn)

 

注意:有人觉得前缀和就行了,但是你还要维护啊,改变某个值,一个一个改变前缀和就是o(n)了。

线段树树状数组的题就是这样,维护一个树,比较容易看出来。

 

 

线段树:

https://blog.csdn.net/hebtu666/article/details/82691008

如果使用线段树,只需要对网址中的实现稍微修改即可。以前维护最小值,现在维护和而已。

注意:要求只是求出前i项,而并未给定一个区间,那我们就能想出更快速、方便的方法。

对于任意一个节点,作为右孩子,如果求和时被用到,那它的左兄弟一定也会被用到,那我们就没必要再用右孩子,因为用他们的父就可以了。

这样一来,我们就可以把所有有孩子全部去掉

把剩下的节点编号。

 转存失败重新上传取消 

如图,可以发现一些规律:1,3,5,7,9等奇数,区间长度都为1

6,10,14等长度为2

........................

如果我们吧编号换成二进制,就能发现,二进制以1结尾的数字区间长度为1,最后有一个零的区间为2,两个零的区间为4.

我们利用二进制就能很容易地把编号和区间对应起来。

 

计算前i项和。

需要把当前编号i的数值加进来,把i最右边的1减掉,直到i变为0.

二进制最后一个1可以通过i&-i得到。

 

更新:

不断把当前位置i加x,把i的二进制最低非零位对应的幂加到i上。

下面是代码:

思想想出来挺麻烦,代码实现很简单,我都不知道要注释点啥

向发明这些东西的大佬们致敬

 
  1. int bit[MAX_N+1]

  2. int n;

  3.  
  4. int sum(int i)

  5. {

  6. int gg=0;

  7. while(i>0)

  8. {

  9. gg+=bit[i];

  10. i-=i&-i;

  11. }

  12. return gg;

  13. }

  14.  
  15. void add(int i,int x)

  16. {

  17. while(i<=n)

  18. {

  19. bit[i]+=x;

  20. i+=i&-i;

  21. }

  22. }

最大搜索子树

给定一个二叉树的头结点,返回最大搜索子树的大小。

 

我们先定义结点:

 
  1. public static class Node {

  2. public int value;

  3. public Node left;

  4. public Node right;

  5.  
  6. public Node(int data) {

  7. this.value = data;

  8. }

  9. }

分析:

直接判断每个节点左边小右边大是不对滴

 

可以暴力判断所有的子树,就不说了。

 

最大搜索子树可能性:

第一种可能性,以node为头的结点的最大二叉搜索子树可能来自它左子树;
第二种可能性,以node为头的结点的最大二叉搜索子树可能来自它右子树;
第三种可能性,左树整体是搜索二叉树,右树整体也是搜索二叉树,而且左树的头是node.left,右树的头是node.right,且左树的最大值< node.value,右树的最小值 > node.value, 那么以我为头的整棵树都是搜索二叉树;
 

第三种可能性的判断,需要的信息有:左子树的最大值、右子树的最小值、左子树是不是搜索二叉树、右子树是不是搜索二叉树

还有左右搜索二叉树的最大深度。

我们判断了自己,并不知道自己是哪边的子树,我们要返回自己的最大值和最小值。

这样,定义一个返回类型:

 
  1. public static class ReturnType{

  2. public int size;//最大搜索子树深度

  3. public Node head;//最大搜索子树的根

  4. public int min;//子树最小

  5. public int max;//子树最大

  6.  
  7. public ReturnType(int a, Node b,int c,int d) {

  8. this.size =a;

  9. this.head = b;

  10. this.min = c;

  11. this.max = d;

  12. }

  13. }

然后开始写代码:

注意:

1)NULL返回深度0,头为NULL,最大值最小值返回系统最大和最小,这样才不会影响别的判断。

 
  1. public static ReturnType process(Node head) {

  2. if(head == null) {

  3. return new ReturnType(0,null,Integer.MAX_VALUE, Integer.MIN_VALUE);

  4. }

  5.  
  6. Node left = head.left;//取信息

  7. ReturnType leftSubTressInfo = process(left);

  8. Node right = head.right;

  9. ReturnType rightSubTressInfo = process(right);

  10.  
  11. int includeItSelf = 0;

  12. if(leftSubTressInfo.head == left // 左子树为搜索树

  13. &&rightSubTressInfo.head == right// 右子树为搜索树

  14. && head.value > leftSubTressInfo.max// 左子树最大值小于当前节点

  15. && head.value < rightSubTressInfo.min//右子树最小值大于当前节点

  16. ) {

  17. includeItSelf = leftSubTressInfo.size + 1 + rightSubTressInfo.size;//当前节点为根的二叉树为搜索树

  18. }

  19.  
  20. int p1 = leftSubTressInfo.size;

  21. int p2 = rightSubTressInfo.size;

  22.  
  23. int maxSize = Math.max(Math.max(p1, p2), includeItSelf);//最大搜索树深度

  24.  
  25. Node maxHead = p1 > p2 ? leftSubTressInfo.head : rightSubTressInfo.head;

  26.  
  27. if(maxSize == includeItSelf) {

  28. maxHead = head;

  29. }//最大搜索树的根:来自左子树、来自右子树、本身

  30.  
  31. return new ReturnType(

  32. maxSize, //深度

  33. maxHead, //根

  34. Math.min(Math.min(leftSubTressInfo.min,rightSubTressInfo.min),head.value), //最小

  35. Math.max(Math.max(leftSubTressInfo.max,rightSubTressInfo.max),head.value)); //最大

  36. }

可以进一步改进:

空间浪费比较严重

其实返回值为三个int,一个node,我们可以把三个int合起来,用全局数组记录,函数只返回node(搜索树的根)即可。

给出完整代码:

 
  1. public class BiggestSubBSTInTree {

  2.  
  3. public static class Node {

  4. public int value;

  5. public Node left;

  6. public Node right;

  7.  
  8. public Node(int data) {

  9. this.value = data;

  10. }

  11. }

  12.  
  13. public static Node biggestSubBST(Node head) {

  14. int[] record = new int[3]; // 0->size, 1->min, 2->max

  15. return posOrder(head, record);

  16. }

  17.  
  18. public static class ReturnType{

  19. public int size;//最大搜索子树深度

  20. public Node head;//最大搜索子树的根

  21. public int min;//子树最小

  22. public int max;//子树最大

  23.  
  24. public ReturnType(int a, Node b,int c,int d) {

  25. this.size =a;

  26. this.head = b;

  27. this.min = c;

  28. this.max = d;

  29. }

  30. }

  31.  
  32. public static ReturnType process(Node head) {

  33. if(head == null) {

  34. return new ReturnType(0,null,Integer.MAX_VALUE, Integer.MIN_VALUE);

  35. }

  36.  
  37. Node left = head.left;//取信息

  38. ReturnType leftSubTressInfo = process(left);

  39. Node right = head.right;

  40. ReturnType rightSubTressInfo = process(right);

  41.  
  42. int includeItSelf = 0;

  43. if(leftSubTressInfo.head == left // 左子树为搜索树

  44. &&rightSubTressInfo.head == right// 右子树为搜索树

  45. && head.value > leftSubTressInfo.max// 左子树最大值小于当前节点

  46. && head.value < rightSubTressInfo.min//右子树最小值大于当前节点

  47. ) {

  48. includeItSelf = leftSubTressInfo.size + 1 + rightSubTressInfo.size;//当前节点为根的二叉树为搜索树

  49. }

  50.  
  51. int p1 = leftSubTressInfo.size;

  52. int p2 = rightSubTressInfo.size;

  53.  
  54. int maxSize = Math.max(Math.max(p1, p2), includeItSelf);//最大搜索树深度

  55.  
  56. Node maxHead = p1 > p2 ? leftSubTressInfo.head : rightSubTressInfo.head;

  57. if(maxSize == includeItSelf) {

  58. maxHead = head;

  59. }//最大搜索树的根:来自左子树、来自右子树、本身

  60.  
  61. return new ReturnType(

  62. maxSize, //深度

  63. maxHead, //根

  64. Math.min(Math.min(leftSubTressInfo.min,rightSubTressInfo.min),head.value), //最小

  65. Math.max(Math.max(leftSubTressInfo.max,rightSubTressInfo.max),head.value)); //最大

  66. }

  67.  
  68.  
  69.  
  70.  
  71. public static Node posOrder(Node head, int[] record) {

  72. if (head == null) {

  73. record[0] = 0;

  74. record[1] = Integer.MAX_VALUE;

  75. record[2] = Integer.MIN_VALUE;

  76. return null;

  77. }

  78. int value = head.value;

  79. Node left = head.left;

  80. Node right = head.right;

  81. Node lBST = posOrder(left, record);

  82. int lSize = record[0];

  83. int lMin = record[1];

  84. int lMax = record[2];

  85. Node rBST = posOrder(right, record);

  86. int rSize = record[0];

  87. int rMin = record[1];

  88. int rMax = record[2];

  89. record[1] = Math.min(rMin, Math.min(lMin, value)); // lmin, value, rmin -> min

  90. record[2] = Math.max(lMax, Math.max(rMax, value)); // lmax, value, rmax -> max

  91. if (left == lBST && right == rBST && lMax < value && value < rMin) {

  92. record[0] = lSize + rSize + 1;//修改深度

  93. return head; //返回根

  94. }//满足当前构成搜索树的条件

  95. record[0] = Math.max(lSize, rSize);//较大深度

  96. return lSize > rSize ? lBST : rBST;//返回较大搜索树的根

  97. }

  98.  
  99. // for test -- print tree

  100. public static void printTree(Node head) {

  101. System.out.println("Binary Tree:");

  102. printInOrder(head, 0, "H", 17);

  103. System.out.println();

  104. }

  105.  
  106. public static void printInOrder(Node head, int height, String to, int len) {

  107. if (head == null) {

  108. return;

  109. }

  110. printInOrder(head.right, height + 1, "v", len);

  111. String val = to + head.value + to;

  112. int lenM = val.length();

  113. int lenL = (len - lenM) / 2;

  114. int lenR = len - lenM - lenL;

  115. val = getSpace(lenL) + val + getSpace(lenR);

  116. System.out.println(getSpace(height * len) + val);

  117. printInOrder(head.left, height + 1, "^", len);

  118. }

  119.  
  120. public static String getSpace(int num) {

  121. String space = " ";

  122. StringBuffer buf = new StringBuffer("");

  123. for (int i = 0; i < num; i++) {

  124. buf.append(space);

  125. }

  126. return buf.toString();

  127. }

  128.  
  129. public static void main(String[] args) {

  130.  
  131. Node head = new Node(6);

  132. head.left = new Node(1);

  133. head.left.left = new Node(0);

  134. head.left.right = new Node(3);

  135. head.right = new Node(12);

  136. head.right.left = new Node(10);

  137. head.right.left.left = new Node(4);

  138. head.right.left.left.left = new Node(2);

  139. head.right.left.left.right = new Node(5);

  140. head.right.left.right = new Node(14);

  141. head.right.left.right.left = new Node(11);

  142. head.right.left.right.right = new Node(15);

  143. head.right.right = new Node(13);

  144. head.right.right.left = new Node(20);

  145. head.right.right.right = new Node(16);

  146.  
  147. printTree(head);

  148. Node bst = biggestSubBST(head);

  149. printTree(bst);

  150.  
  151. }

  152.  
  153. }

morris遍历

通常,实现二叉树的前序(preorder)、中序(inorder)、后序(postorder)遍历有两个常用的方法:一是递归(recursive),二是使用栈实现的迭代版本(stack+iterative)。这两种方法都是O(n)的空间复杂度(递归本身占用stack空间或者用户自定义的stack)。

本文介绍空间O(1)的遍历方法。

上次文章讲到,我们经典递归遍历其实有三次访问当前节点的机会,就看你再哪次进行操作,而分成了三种遍历。

https://blog.csdn.net/hebtu666/article/details/82853988

morris有两次访问节点的机会。

它省空间的原理是利用了大量叶子节点的没有用的空间,记录之前的节点,做到了返回之前节点这件事情。

我们不说先序中序后序,先说morris遍历的原则:

1、如果没有左孩子,继续遍历右子树

2、如果有左孩子,找到左子树最右节点。

    1)如果最右节点的右指针为空(说明第一次遇到),把它指向当前节点,当前节点向左继续处理。

    2)如果最右节点的右指针不为空(说明它指向之前结点),把右指针设为空,当前节点向右继续处理。

 

这就是morris遍历。

请手动模拟深度至少为3的树的morris遍历来熟悉流程。

 

先看代码:

定义结点:

 
  1. public static class Node {

  2. public int value;

  3. Node left;

  4. Node right;

  5.  
  6. public Node(int data) {

  7. this.value = data;

  8. }

  9. }

先序:

 (完全按规则写就好。)

 
  1. //打印时机(第一次遇到):发现左子树最右的孩子右指针指向空,或无左子树。

  2. public static void morrisPre(Node head) {

  3. if (head == null) {

  4. return;

  5. }

  6. Node cur1 = head;

  7. Node cur2 = null;

  8. while (cur1 != null) {

  9. cur2 = cur1.left;

  10. if (cur2 != null) {

  11. while (cur2.right != null && cur2.right != cur1) {

  12. cur2 = cur2.right;

  13. }

  14. if (cur2.right == null) {

  15. cur2.right = cur1;

  16. System.out.print(cur1.value + " ");

  17. cur1 = cur1.left;

  18. continue;

  19. } else {

  20. cur2.right = null;

  21. }

  22. } else {

  23. System.out.print(cur1.value + " ");

  24. }

  25. cur1 = cur1.right;

  26. }

  27. System.out.println();

  28. }

morris在发表文章时只写出了中序遍历。而先序遍历只是打印时机不同而已,所以后人改进出了先序遍历。至于后序,是通过打印所有的右边界来实现的:对每个有边界逆序,打印,再逆序回去。注意要原地逆序,否则我们morris遍历的意义也就没有了。

完整代码: 

 
  1. public class MorrisTraversal {

  2.  
  3.  
  4.  
  5. public static void process(Node head) {

  6. if(head == null) {

  7. return;

  8. }

  9.  
  10. // 1

  11. //System.out.println(head.value);

  12.  
  13.  
  14. process(head.left);

  15.  
  16. // 2

  17. //System.out.println(head.value);

  18.  
  19.  
  20. process(head.right);

  21.  
  22. // 3

  23. //System.out.println(head.value);

  24. }

  25.  
  26.  
  27. public static class Node {

  28. public int value;

  29. Node left;

  30. Node right;

  31.  
  32. public Node(int data) {

  33. this.value = data;

  34. }

  35. }

  36. //打印时机:向右走之前

  37. public static void morrisIn(Node head) {

  38. if (head == null) {

  39. return;

  40. }

  41. Node cur1 = head;//当前节点

  42. Node cur2 = null;//最右

  43. while (cur1 != null) {

  44. cur2 = cur1.left;

  45. //左孩子不为空

  46. if (cur2 != null) {

  47. while (cur2.right != null && cur2.right != cur1) {

  48. cur2 = cur2.right;

  49. }//找到最右

  50. //右指针为空,指向cur1,cur1向左继续

  51. if (cur2.right == null) {

  52. cur2.right = cur1;

  53. cur1 = cur1.left;

  54. continue;

  55. } else {

  56. cur2.right = null;

  57. }//右指针不为空,设为空

  58. }

  59. System.out.print(cur1.value + " ");

  60. cur1 = cur1.right;

  61. }

  62. System.out.println();

  63. }

  64. //打印时机(第一次遇到):发现左子树最右的孩子右指针指向空,或无左子树。

  65. public static void morrisPre(Node head) {

  66. if (head == null) {

  67. return;

  68. }

  69. Node cur1 = head;

  70. Node cur2 = null;

  71. while (cur1 != null) {

  72. cur2 = cur1.left;

  73. if (cur2 != null) {

  74. while (cur2.right != null && cur2.right != cur1) {

  75. cur2 = cur2.right;

  76. }

  77. if (cur2.right == null) {

  78. cur2.right = cur1;

  79. System.out.print(cur1.value + " ");

  80. cur1 = cur1.left;

  81. continue;

  82. } else {

  83. cur2.right = null;

  84. }

  85. } else {

  86. System.out.print(cur1.value + " ");

  87. }

  88. cur1 = cur1.right;

  89. }

  90. System.out.println();

  91. }

  92. //逆序打印所有右边界

  93. public static void morrisPos(Node head) {

  94. if (head == null) {

  95. return;

  96. }

  97. Node cur1 = head;

  98. Node cur2 = null;

  99. while (cur1 != null) {

  100. cur2 = cur1.left;

  101. if (cur2 != null) {

  102. while (cur2.right != null && cur2.right != cur1) {

  103. cur2 = cur2.right;

  104. }

  105. if (cur2.right == null) {

  106. cur2.right = cur1;

  107. cur1 = cur1.left;

  108. continue;

  109. } else {

  110. cur2.right = null;

  111. printEdge(cur1.left);

  112. }

  113. }

  114. cur1 = cur1.right;

  115. }

  116. printEdge(head);

  117. System.out.println();

  118. }

  119. //逆序打印

  120. public static void printEdge(Node head) {

  121. Node tail = reverseEdge(head);

  122. Node cur = tail;

  123. while (cur != null) {

  124. System.out.print(cur.value + " ");

  125. cur = cur.right;

  126. }

  127. reverseEdge(tail);

  128. }

  129. //逆序(类似链表逆序)

  130. public static Node reverseEdge(Node from) {

  131. Node pre = null;

  132. Node next = null;

  133. while (from != null) {

  134. next = from.right;

  135. from.right = pre;

  136. pre = from;

  137. from = next;

  138. }

  139. return pre;

  140. }

  141. public static void main(String[] args) {

  142. Node head = new Node(4);

  143. head.left = new Node(2);

  144. head.right = new Node(6);

  145. head.left.left = new Node(1);

  146. head.left.right = new Node(3);

  147. head.right.left = new Node(5);

  148. head.right.right = new Node(7);

  149.  
  150. morrisIn(head);

  151. morrisPre(head);

  152. morrisPos(head);

  153. }

  154.  
  155. }

最小生成树

 

问题提出:
    要在n个城市间建立通信联络网。顶点:表示城市,权:城市间通信线路的花费代价。希望此通信网花费代价最小。
问题分析:
    答案只能从生成树中找,因为要做到任何两个城市之间有线路可达,通信网必须是连通的;但对长度最小的要求可以知道网中显然不能有圈,如果有圈,去掉一条边后,并不破坏连通性,但总代价显然减少了,这与总代价最小的假设是矛盾的。
结论:
    希望找到一棵生成树,它的每条边上的权值之和(即建立该通信网所需花费的总代价)最小 —— 最小代价生成树。
    构造最小生成树的算法很多,其中多数算法都利用了一种称之为 MST 的性质。
    MST 性质:设 N = (V, E)  是一个连通网,U是顶点集 V的一个非空子集。若边 (u, v) 是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边 (u, v) 的最小生成树。


(1)普里姆 (Prim) 算法

算法思想: 
    ①设 N=(V, E)是连通网,TE是N上最小生成树中边的集合。
    ②初始令 U={u_0}, (u_0∈V), TE={ }。
    ③在所有u∈U,u∈U-V的边(u,v)∈E中,找一条代价最小的边(u_0,v_0 )。
    ④将(u_0,v_0 )并入集合TE,同时v_0并入U。
    ⑤重复上述操作直至U = V为止,则 T=(V,TE)为N的最小生成树。

 
代码实现:

 
  1. void MiniSpanTree_PRIM(MGraph G,VertexType u)

  2.     //用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边。

  3.     //记录从顶点集U到V-U的代价最小的边的辅助数组定义;

  4.     //closedge[j].lowcost表示在集合U中顶点与第j个顶点对应最小权值

  5. {

  6.     int k, j, i;

  7.     k = LocateVex(G,u);

  8.     for (j = 0; j < G.vexnum; ++j)    //辅助数组的初始化

  9.         if(j != k)

  10.         {

  11.             closedge[j].adjvex = u;

  12.             closedge[j].lowcost = G.arcs[k][j].adj;    

  13. //获取邻接矩阵第k行所有元素赋给closedge[j!= k].lowcost

  14.         }

  15.     closedge[k].lowcost = 0;        

  16. //初始,U = {u};  

  17.     PrintClosedge(closedge,G.vexnum);

  18.     for (i = 1; i < G.vexnum; ++i)    \

  19. //选择其余G.vexnum-1个顶点,因此i从1开始循环

  20.     {

  21.         k = minimum(G.vexnum,closedge);        

  22. //求出最小生成树的下一个结点:第k顶点

  23.         PrintMiniTree_PRIM(G, closedge, k);     //输出生成树的边

  24.         closedge[k].lowcost = 0;                //第k顶点并入U集

  25.         PrintClosedge(closedge,G.vexnum);

  26.         for(j = 0;j < G.vexnum; ++j)

  27.         {                                           

  28.             if(G.arcs[k][j].adj < closedge[j].lowcost)    

  29. //比较第k个顶点和第j个顶点权值是否小于closedge[j].lowcost

  30.             {

  31.                 closedge[j].adjvex = G.vexs[k];//替换closedge[j]

  32.                 closedge[j].lowcost = G.arcs[k][j].adj;

  33.                 PrintClosedge(closedge,G.vexnum);

  34.             }

  35.         }

  36.     }

  37. }


(2)克鲁斯卡尔 (Kruskal) 算法

算法思想: 
    ①设连通网  N = (V, E ),令最小生成树初始状态为只有n个顶点而无边的非连通图,T=(V, { }),每个顶点自成一个连通分量。
    ②在 E 中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边。
③依此类推,直至 T 中所有顶点都在同一连通分量上为止。
      
    最小生成树可能不惟一!

 

拓扑排序

 

(1)有向无环图

    无环的有向图,简称 DAG (Directed Acycline Graph) 图。
 
有向无环图在工程计划和管理方面的应用:除最简单的情况之外,几乎所有的工程都可分为若干个称作“活动”的子工程,并且这些子工程之间通常受着一定条件的约束,例如:其中某些子工程必须在另一些子工程完成之后才能开始。
对整个工程和系统,人们关心的是两方面的问题: 
①工程能否顺利进行; 
②完成整个工程所必须的最短时间。

对应到有向图即为进行拓扑排序和求关键路径。 
AOV网: 
    用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex network)。
例如:排课表
      
AOV网的特点:
①若从i到j有一条有向路径,则i是j的前驱;j是i的后继。
②若< i , j >是网中有向边,则i是j的直接前驱;j是i的直接后继。
③AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。


问题:    
    问题:如何判别 AOV 网中是否存在回路?
    检测 AOV 网中是否存在环方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。


拓扑排序的方法:
    ①在有向图中选一个没有前驱的顶点且输出之。
    ②从图中删除该顶点和所有以它为尾的弧。
    ③重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止。
        
    一个AOV网的拓扑序列不是唯一的!
代码实现:

 
  1. Status TopologicalSort(ALGraph G)

  2.     //有向图G采用邻接表存储结构。

  3.     //若G无回路,则输出G的顶点的一个拓扑序列并返回OK,否则返回ERROR.

  4.     //输出次序按照栈的后进先出原则,删除顶点,输出遍历

  5. {

  6.     SqStack S;

  7.     int i, count;

  8.     int *indegree1 = (int *)malloc(sizeof(int) * G.vexnum);

  9.     int indegree[12] = {0};

  10.     FindInDegree(G, indegree);    //求个顶点的入度下标从0开始

  11.     InitStack(&S);

  12.     PrintStack(S);

  13.     for(i = 0; i < G.vexnum; ++i)

  14.         if(!indegree[i])        //建0入度顶点栈S

  15.             push(&S,i);        //入度为0者进栈

  16.     count = 0;                //对输出顶点计数

  17.     while (S.base != S.top)

  18.     {

  19.         ArcNode* p;

  20.         pop(&S,&i);

  21.         VisitFunc(G,i);//第i个输出栈顶元素对应的顶点,也就是最后进来的顶点    

  22.         ++count;          //输出i号顶点并计数

  23.         for(p = G.vertices[i].firstarc; p; p = p->nextarc)

  24.         {    //通过循环遍历第i个顶点的表结点,将表结点中入度都减1

  25.             int k = p->adjvex;    //对i号顶点的每个邻接点的入度减1

  26.             if(!(--indegree[k]))

  27.                 push(&S,k);        //若入度减为0,则入栈

  28.         }//for

  29.     }//while

  30.     if(count < G.vexnum)

  31.     {

  32.         printf("\n该有向图有回路!\n");

  33.         return ERROR;    //该有向图有回路

  34.     }

  35.     else

  36.     {

  37.         printf("\n该有向图没有回路!\n");

  38.         return OK;

  39.     }

  40. }


关键路径

    把工程计划表示为有向图,用顶点表示事件,弧表示活动,弧的权表示活动持续时间。每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。称这种有向图为边表示活动的网,简称为 AOE网 (Activity On Edge)。
例如:
设一个工程有11项活动,9个事件。
事件v_1——表示整个工程开始(源点) 
事件v_9——表示整个工程结束(汇点)

 
对AOE网,我们关心两个问题:  
①完成整项工程至少需要多少时间? 
②哪些活动是影响工程进度的关键?
关键路径——路径长度最长的路径。
路径长度——路径上各活动持续时间之和。
v_i——表示事件v_i的最早发生时间。假设开始点是v_1,从v_1到〖v�i〗的最长路径长度。ⅇ(ⅈ)——表示活动a_i的最早发生时间。
l(ⅈ)——表示活动a_i最迟发生时间。在不推迟整个工程完成的前提下,活动a_i最迟必须开始进行的时间。
l(ⅈ)-ⅇ(ⅈ)意味着完成活动a_i的时间余量。
我们把l(ⅈ)=ⅇ(ⅈ)的活动叫做关键活动。显然,关键路径上的所有活动都是关键活动,因此提前完成非关键活动并不能加快工程进度。
    例如上图中网,从从v_1到v_9的最长路径是(v_1,v_2,v_5,v_8,ν_9 ),路径长度是18,即ν_9的最迟发生时间是18。而活动a_6的最早开始时间是5,最迟开始时间是8,这意味着:如果a_6推迟3天或者延迟3天完成,都不会影响整个工程的完成。因此,分析关键路径的目的是辨别哪些是关键活动,以便争取提高关键活动的工效,缩短整个工期。
    由上面介绍可知:辨别关键活动是要找l(ⅈ)=ⅇ(ⅈ)的活动。为了求ⅇ(ⅈ)和l(ⅈ),首先应求得事件的最早发生时间vⅇ(j)和最迟发生时间vl(j)。如果活动a_i由弧〈j,k〉表示,其持续时间记为dut(〈j,k〉),则有如下关系:
ⅇ(ⅈ)= vⅇ(j)
l(ⅈ)=vl(k)-dut(〈j,k〉)
    求vⅇ(j)和vl(j)需分两步进行:
第一步:从vⅇ(0)=0开始向前递推
vⅇ(j)=Max{vⅇ(i)+dut(〈j,k〉)}   〈i,j〉∈T,j=1,2,…,n-1
其中,T是所有以第j个顶点为头的弧的集合。
第二步:从vl(n-1)=vⅇ(n-1)起向后递推
vl(i)=Min{vl(j)-dut(〈i,j〉)}  〈i,j〉∈S,i=n-2,…,0
其中,S是所有以第i个顶点为尾的弧的集合。
下面我们以上图AOE网为例,先求每个事件v_i的最早发生时间,再逆向求每个事件对应的最晚发生时间。再求每个活动的最早发生时间和最晚发生时间,如下面表格:
          
在活动的统计表中,活动的最早发生时间和最晚发生时间相等的,就是关键活动


关键路径的讨论:

①若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。      如:a11、a10、a8、a7。 
②如果一个活动处于所有的关键路径上,则提高这个活动的速度,就能缩短整个工程的完成时间。如:a1、a4。
③处于所有关键路径上的活动完成时间不能缩短太多,否则会使原关键路径变成非关键路径。这时必须重新寻找关键路径。如:a1由6天变成3天,就会改变关键路径。

关键路径算法实现:

 
  1. int CriticalPath(ALGraph G)

  2. {    //因为G是有向网,输出G的各项关键活动

  3.     SqStack T;

  4.     int i, j;    ArcNode* p;

  5.     int k , dut;

  6.     if(!TopologicalOrder(G,T))

  7.         return 0;

  8.     int vl[VexNum];

  9.     for (i = 0; i < VexNum; i++)

  10.         vl[i] = ve[VexNum - 1];        //初始化顶点事件的最迟发生时间

  11.     while (T.base != T.top)            //按拓扑逆序求各顶点的vl值

  12.     {

  13.  

  14.         for(pop(&T, &j), p = G.vertices[j].firstarc; p; p = p->nextarc)

  15.         {

  16.             k = p->adjvex;    dut = *(p->info);    //dut<j, k>

  17.             if(vl[k] - dut < vl[j])

  18.                 vl[j] = vl[k] - dut;

  19.         }//for

  20.     }//while

  21.     for(j = 0; j < G.vexnum; ++j)    //求ee,el和关键活动

  22.     {

  23.         for (p = G.vertices[j].firstarc; p; p = p->nextarc)

  24.         {

  25.             int ee, el;        char tag;

  26.             k = p->adjvex;    dut = *(p->info);

  27.             ee = ve[j];    el = vl[k] - dut;

  28.             tag = (ee == el) ? '*' : ' ';

  29.             PrintCriticalActivity(G,j,k,dut,ee,el,tag);

  30.         }

  31.     }

  32.     return 1;

  33. }

最短路

 

最短路

    典型用途:交通网络的问题——从甲地到乙地之间是否有公路连通?在有多条通路的情况下,哪一条路最短?
 
    交通网络用有向网来表示:顶点——表示城市,弧——表示两个城市有路连通,弧上的权值——表示两城市之间的距离、交通费或途中所花费的时间等。
    如何能够使一个城市到另一个城市的运输时间最短或运费最省?这就是一个求两座城市间的最短路径问题。
    问题抽象:在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n - 1条边。
   常见最短路径问题:单源点最短路径、所有顶点间的最短路径
(1)如何求得单源点最短路径?
    穷举法:将源点到终点的所有路径都列出来,然后在其中选最短的一条。但是,当路径特别多时,特别麻烦;没有规律可循。
    迪杰斯特拉(Dijkstra)算法:按路径长度递增次序产生各顶点的最短路径。
路径长度最短的最短路径的特点:
    在此路径上,必定只含一条弧 <v_0, v_1>,且其权值最小。由此,只要在所有从源点出发的弧中查找权值最小者。
下一条路径长度次短的最短路径的特点:
①、直接从源点到v_2<v_0, v_2>(只含一条弧);
②、从源点经过顶点v_1,再到达v_2<v_0, v_1>,<v_1, v_2>(由两条弧组成)
再下一条路径长度次短的最短路径的特点:
    有以下四种情况:
    ①、直接从源点到v_3<v_0, v_3>(由一条弧组成);
    ②、从源点经过顶点v_1,再到达v_3<v_0, v_1>,<v_1, v_3>(由两条弧组成);
    ③、从源点经过顶点v_2,再到达v_3<v_0, v_2>,<v_2, v_3>(由两条弧组成);
    ④、从源点经过顶点v_1  ,v_2,再到达v_3<v_0, v_1>,<v_1, v_2>,<v_2, v_3>(由三条弧组成);
其余最短路径的特点:    
    ①、直接从源点到v_i<v_0, v_i>(只含一条弧);
    ②、从源点经过已求得的最短路径上的顶点,再到达v_i(含有多条弧)。
Dijkstra算法步骤:
    初始时令S={v_0},  T={其余顶点}。T中顶点对应的距离值用辅助数组D存放。
    D[i]初值:若<v_0, v_i>存在,则为其权值;否则为∞。 
    从T中选取一个其距离值最小的顶点v_j,加入S。对T中顶点的距离值进行修改:若加进v_j作中间顶点,从v_0到v_i的距离值比不加 vj 的路径要短,则修改此距离值。
    重复上述步骤,直到 S = V 为止。

算法实现:

 
  1. void ShortestPath_DIJ(MGraph G,int v0,PathMatrix &P,ShortPathTable &D)

  2. { // 用Dijkstra算法求有向网 G 的 v0 顶点到其余顶点v的最短路径P[v]及带权长度D[v]。

  3.     // 若P[v][w]为TRUE,则 w 是从 v0 到 v 当前求得最短路径上的顶点。  P是存放最短路径的矩阵,经过顶点变成TRUE

  4.     // final[v]为TRUE当且仅当 v∈S,即已经求得从v0到v的最短路径。

  5.     int v,w,i,j,min;

  6.     Status final[MAX_VERTEX_NUM];

  7.     for(v = 0 ;v < G.vexnum ;++v)

  8.     {

  9.         final[v] = FALSE;

  10.         D[v] = G.arcs[v0][v].adj;        //将顶点数组中下标对应是 v0 和 v的距离给了D[v]

  11.         for(w = 0;w < G.vexnum; ++w)

  12.             P[v][w] = FALSE;            //设空路径

  13.         if(D[v] < INFINITY)

  14.         {

  15.             P[v][v0] = TRUE;

  16.             P[v][v] = TRUE;

  17.         }

  18.     }

  19.     D[v0]=0;

  20.     final[v0]= TRUE; /* 初始化,v0顶点属于S集 */

  21.     for(i = 1;i < G.vexnum; ++i) /* 其余G.vexnum-1个顶点 */

  22.     { /* 开始主循环,每次求得v0到某个v顶点的最短路径,并加v到S集 */

  23.         min = INFINITY; /* 当前所知离v0顶点的最近距离 */

  24.         for(w = 0;w < G.vexnum; ++w)

  25.             if(!final[w]) /* w顶点在V-S中 */

  26.                 if(D[w] < min)

  27.                 {

  28.                     v = w;

  29.                     min = D[w];

  30.                 } /* w顶点离v0顶点更近 */

  31.                 final[v] = TRUE; /* 离v0顶点最近的v加入S集 */

  32.                 for(w = 0;w < G.vexnum; ++w) /* 更新当前最短路径及距离 */

  33.                 {

  34.                     if(!final[w] && min < INFINITY && G.arcs[v][w].adj < INFINITY && (min + G.arcs[v][w].adj < D[w]))

  35.                     { /* 修改D[w]和P[w],w∈V-S */

  36.                         D[w] = min + G.arcs[v][w].adj;

  37.                         for(j = 0;j < G.vexnum;++j)

  38.                             P[w][j] = P[v][j];

  39.                         P[w][w] = TRUE;

  40.                     }

  41.                 }

  42.     }

  43. }

简单迷宫问题

迷宫实验是取自心理学的一个古典实验。在该实验中,把一只老鼠从一个无顶大盒子的门放入,在盒子中设置了许多墙,对行进方向形成了多处阻挡。盒子仅有一个出口,在出口处放置一块奶酪,吸引老鼠在迷宫中寻找道路以到达出口。对同一只老鼠重复进行上述实验,一直到老鼠从入口到出口,而不走错一步。老鼠经过多次试验终于得到它学习走通迷宫的路线。设计一个计算机程序对任意设定的迷宫,求出一条从入口到出口的通路,或得出没有通路的结论。
数组元素值为1表示该位置是墙壁,不能通行;元素值为0表示该位置是通路。假定从mg[1][1]出发,出口位于mg[n][m]

用一种标志在二维数组中标出该条通路,并在屏幕上输出二维数组。

 
  1. m=[[1,1,1,0,1,1,1,1,1,1],

  2. [1,0,0,0,0,0,0,0,1,1],

  3. [1,0,1,1,1,1,1,0,0,1],

  4. [1,0,1,0,0,0,0,1,0,1],

  5. [1,0,1,0,1,1,0,0,0,1],

  6. [1,0,0,1,1,0,1,0,1,1],

  7. [1,1,1,1,0,0,0,0,1,1],

  8. [1,0,0,0,0,1,1,1,0,0],

  9. [1,0,1,1,0,0,0,0,0,1],

  10. [1,1,1,1,1,1,1,1,1,1]]

  11. sta1=0;sta2=3;fsh1=7;fsh2=9;success=0

  12. def LabyrinthRat():

  13. print('显示迷宫:')

  14. for i in range(len(m)):print(m[i])

  15. print('入口:m[%d][%d]:出口:m[%d][%d]'%(sta1,sta2,fsh1,fsh2))

  16. if (visit(sta1,sta2))==0: print('没有找到出口')

  17. else:

  18. print('显示路径:')

  19. for i in range(10):print(m[i])

  20. def visit(i,j):

  21. m[i][j]=2

  22. global success

  23. if(i==fsh1)and(j==fsh2): success=1

  24. if(success!=1)and(m[i-1][j]==0): visit(i-1,j)

  25. if(success!=1)and(m[i+1][j]==0): visit(i+1,j)

  26. if(success!=1)and(m[i][j-1]==0): visit(i,j-1)

  27. if(success!=1)and(m[i][j+1]==0): visit(i,j+1)

  28. if success!=1: m[i][j]=3

  29. return success

  30. LabyrinthRat()

深搜DFS\广搜BFS 

首先,不管是BFS还是DFS,由于时间和空间的局限性,它们只能解决数据量比较小的问题。

深搜,顾名思义,它从某个状态开始,不断的转移状态,直到无法转移,然后退回到上一步的状态,继续转移到其他状态,不断重复,直到找到最终的解。从实现上来说,栈结构是后进先出,可以很好的保存上一步状态并利用。所以根据深搜和栈结构的特点,深度优先搜索利用递归函数(栈)来实现,只不过这个栈是系统帮忙做的,不太明显罢了。

 

广搜和深搜的搜索顺序不同,它是先搜索离初始状态比较近的状态,搜索顺序是这样的:初始状态---------->一步能到的状态--------->两步能到的状态......从实现上说,它是通过队列实现的,并且是我们自己做队列。一般解决最短路问题,因为第一个搜到的一定是最短路。

下面通过两道简单例题简单的入个门。

深搜例题

poj2386

http://poj.org/problem?id=2386

题目大意:上下左右斜着挨着都算一个池子,看图中有几个池子。

 
  1. W........WW.

  2. .WWW.....WWW

  3. ....WW...WW.

  4. .........WW.

  5. .........W..

  6. ..W......W..

  7. .W.W.....WW.

  8. W.W.W.....W.

  9. .W.W......W.

  10. ..W.......W.例如本图就是有三个池子

采用深度优先搜索,从任意的w开始,不断把邻接的部分用'.'代替,1次DFS后与初始这个w连接的所有w就全都被替换成'.',因此直到图中不再存在W为止。

核心代码:

 
  1. char field[maxn][maxn];//图

  2. int n,m;长宽

  3. void dfs(int x,int y)

  4. {

  5. field[x][y]='.';//先做了标记

  6. //循环遍历八个方向

  7. for(int dx=-1;dx<=1;dx++){

  8. for(int dy=-1;dy<=1;dy++){

  9. int nx=x+dx,ny=y+dy;

  10. //判断(nx,ny)是否在园子里,以及是否有积水

  11. if(0<=nx&&nx<n&&0<=ny&&ny<m&&field[nx][ny]=='W'){

  12. dfs(nx,ny);

  13. }

  14. }

  15. }

  16. }

  17. void solve()

  18. {

  19. int res=0;

  20. for(int i=0;i<n;i++){

  21. for(int j=0;j<m;j++){

  22. if(field[i][j]=='W'){

  23. //从有积水的地方开始搜

  24. dfs(i,j);

  25. res++;//搜几次就有几个池子

  26. }

  27. }

  28. }

  29. printf("%d\n",res);

  30. }

广搜例题:

迷宫的最短路径

  给定一个大小为N×M的迷宫。迷宫由通道和墙壁组成,每一步可以向邻接的上下左右四个的通道移动。请求出从起点到终点所需的最小步数。请注意,本题假定从起点一定可以移动到终点。(N,M≤100)('#', '.' , 'S', 'G'分别表示墙壁、通道、起点和终点)

输入:

10 10

#S######.#
......#..#
.#.##.##.#
.#........
##.##.####
....#....#
.#######.#
....#.....
.####.###.
....#...G#

输出:

22

小白书上部分代码:

 
  1. typedef pair<int, int> P;

  2. char maze[maxn][maxn];

  3. int n, m, sx, sy, gx, gy,d[maxn][maxn];//到各个位置的最短距离的数组

  4. int dx[4] = { 1,0,-1,0 }, dy[4]= { 0,1,0,-1 };//4个方向移动的向量

  5. int bfs()//求从(sx,sy)到(gx,gy)的最短距离,若无法到达则是INF

  6. {

  7. queue<P> que;

  8. for (int i = 0; i < n; i++)

  9. for (int j = 0; j < m; j++)

  10. d[i][j] = INF;//所有的位置都初始化为INF

  11. que.push(P(sx, sy));//将起点加入队列中

  12. d[sx][sy] = 0;//并把起点的距离设置为0

  13. while (que.size())//不断循环直到队列的长度为0

  14. {

  15. P p = que.front();// 从队列的最前段取出元素

  16. que.pop();//删除该元素

  17. if (p.first == gx&&p.second == gy)//是终点结束

  18. break;

  19. for (int i = 0; i < 4; i++)//四个方向的循环

  20. {

  21. int nx = p.first + dx[i],ny = p.second + dy[i];//移动后的位置标记为(nx,ny)

  22. if (0 <= nx&&nx < n && 0 <= ny&&ny < m&&maze[nx][ny] != '#'&&d[nx][ny] == INF)//判断是否可以移动以及是否访问过(即d[nx][ny]!=INF)

  23. {

  24. que.push(P(nx, ny));//可以移动,添加到队列

  25. d[nx][ny] = d[p.first][p.second] + 1;//到该位置的距离为到p的距离+1

  26. }

  27. }

  28. }

  29. return d[gx][gy];

  30. }

经典了两个题结束了,好题链接持续更新。。。。。。

 皇后问题

 

八皇后问题是一个以国际象棋为背景的问题:如何能够在 8×8 的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n1×n1,而皇后个数也变成n2。而且仅当 n2 ≥ 1 或 n1 ≥ 4 时问题有解。

皇后问题是非常著名的问题,作为一个棋盘类问题,毫无疑问,用暴力搜索的方法来做是一定可以得到正确答案的,但在有限的运行时间内,我们很难写出速度可以忍受的搜索,部分棋盘问题的最优解不是搜索,而是动态规划,某些棋盘问题也很适合作为状态压缩思想的解释例题。

进一步说,皇后问题可以用人工智能相关算法和遗传算法求解,可以用多线程技术缩短运行时间。本文不做讨论。

(本文不展开讲状态压缩,以后再说)

 

一般思路:

 

N*N的二维数组,在每一个位置进行尝试,在当前位置上判断是否满足放置皇后的条件(这一点的行、列、对角线上,没有皇后)。

 

优化1:

 

既然知道多个皇后不能在同一行,我们何必要在同一行的不同位置放多个来尝试呢?

我们生成一维数组record,record[i]表示第i行的皇后放在了第几列。对于每一行,确定当前record值即可,因为每行只能且必须放一个皇后,放了一个就无需继续尝试。那么对于当前的record[i],查看record[0...i-1]的值,是否有j = record[k](同列)、|record[k] - j| = | k-i |(同一斜线)的情况。由于我们的策略,无需检查行(每行只放一个)。

 
  1. public class NQueens {

  2. public static int num1(int n) {

  3. if (n < 1) {

  4. return 0;

  5. }

  6. int[] record = new int[n];

  7. return process1(0, record, n);

  8. }

  9. public static int process1(int i, int[] record, int n) {

  10. if (i == n) {

  11. return 1;

  12. }

  13. int res = 0;

  14. for (int j = 0; j < n; j++) {

  15. if (isValid(record, i, j)) {

  16. record[i] = j;

  17. res += process1(i + 1, record, n);

  18. }

  19. }//对于当前行,依次尝试每列

  20. return res;

  21. }

  22. //判断当前位置是否可以放置

  23. public static boolean isValid(int[] record, int i, int j) {

  24. for (int k = 0; k < i; k++) {

  25. if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {

  26. return false;

  27. }

  28. }

  29. return true;

  30. }

  31. public static void main(String[] args) {

  32. int n = 8;

  33. System.out.println(num1(n));

  34. }

  35. }

优化2:

 

分析:棋子对后续过程的影响范围:本行、本列、左右斜线。

黑色棋子影响区域为红色

本行影响不提,根据优化一已经避免

本列影响,一直影响D列,直到第一行在D放棋子的所有情况结束。

 

左斜线:每向下一行,实际上对当前行的影响区域就向左移动

比如:

尝试第二行时,黑色棋子影响的是我们的第三列;

尝试第三行时,黑色棋子影响的是我们的第二列;

尝试第四行时,黑色棋子影响的是我们的第一列;

尝试第五行及以后几行,黑色棋子对我们并无影响。

 

右斜线则相反:

随着行序号增加,影响的列序号也增加,直到影响的列序号大于8就不再影响。

 

我们对于之前棋子影响的区域,可以用二进制数字来表示,比如:

每一位,用01代表是否影响。

比如上图,对于第一行,就是00010000

尝试第二行时,数字变为00100000

第三行:01000000

第四行:10000000

 

对于右斜线的数字,同理:

第一行00010000,之后向右移:00001000,00000100,00000010,00000001,直到全0不影响。

 

同理,我们对于多行数据,也同样可以记录了

比如在第一行我们放在了第四列:

第二行放在了G列,这时左斜线记录为00100000(第一个棋子的影响)+00000010(当前棋子的影响)=00100010。

到第三行数字继续左移:01000100,然后继续加上我们的选择,如此反复。

 

这样,我们对于当前位置的判断,其实可以通过左斜线变量、右斜线变量、列变量,按位或运算求出(每一位中,三个数有一个是1就不能再放)。

具体看代码:

注:怎么排版就炸了呢。。。贴一张图吧

 
  1. public class NQueens {

  2. public static int num2(int n) {

  3. // 因为本方法中位运算的载体是int型变量,所以该方法只能算1~32皇后问题

  4. // 如果想计算更多的皇后问题,需使用包含更多位的变量

  5. if (n < 1 || n > 32) {

  6. return 0;

  7. }

  8. int upperLim = n == 32 ? -1 : (1 << n) - 1;

  9. //upperLim的作用为棋盘大小,比如8皇后为00000000 00000000 00000000 11111111

  10. //32皇后为11111111 11111111 11111111 11111111

  11. return process2(upperLim, 0, 0, 0);

  12. }

  13.  
  14. public static int process2(int upperLim, int colLim, int leftDiaLim,

  15. int rightDiaLim) {

  16. if (colLim == upperLim) {

  17. return 1;

  18. }

  19. int pos = 0; //pos:所有的合法位置

  20. int mostRightOne = 0; //所有合法位置的最右位置

  21.  
  22. //所有记录按位或之后取反,并与全1按位与,得出所有合法位置

  23. pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));

  24. int res = 0;//计数

  25. while (pos != 0) {

  26. mostRightOne = pos & (~pos + 1);//取最右的合法位置

  27. pos = pos - mostRightOne; //去掉本位置并尝试

  28. res += process2(

  29. upperLim, //全局

  30. colLim | mostRightOne, //列记录

  31. //之前列+本位置

  32. (leftDiaLim | mostRightOne) << 1, //左斜线记录

  33. //(左斜线变量+本位置)左移

  34. (rightDiaLim | mostRightOne) >>> 1); //右斜线记录

  35. //(右斜线变量+本位置)右移(高位补零)

  36. }

  37. return res;

  38. }

  39.  
  40. public static void main(String[] args) {

  41. int n = 8;

  42. System.out.println(num2(n));

  43. }

  44. }

完整测试代码:

32皇后:结果/时间

暴力搜:时间就太长了,懒得测。。。

 
  1. public class NQueens {

  2.  
  3. public static int num1(int n) {

  4. if (n < 1) {

  5. return 0;

  6. }

  7. int[] record = new int[n];

  8. return process1(0, record, n);

  9. }

  10.  
  11. public static int process1(int i, int[] record, int n) {

  12. if (i == n) {

  13. return 1;

  14. }

  15. int res = 0;

  16. for (int j = 0; j < n; j++) {

  17. if (isValid(record, i, j)) {

  18. record[i] = j;

  19. res += process1(i + 1, record, n);

  20. }

  21. }

  22. return res;

  23. }

  24.  
  25. public static boolean isValid(int[] record, int i, int j) {

  26. for (int k = 0; k < i; k++) {

  27. if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {

  28. return false;

  29. }

  30. }

  31. return true;

  32. }

  33.  
  34. public static int num2(int n) {

  35. if (n < 1 || n > 32) {

  36. return 0;

  37. }

  38. int upperLim = n == 32 ? -1 : (1 << n) - 1;

  39. return process2(upperLim, 0, 0, 0);

  40. }

  41.  
  42. public static int process2(int upperLim, int colLim, int leftDiaLim,

  43. int rightDiaLim) {

  44. if (colLim == upperLim) {

  45. return 1;

  46. }

  47. int pos = 0;

  48. int mostRightOne = 0;

  49. pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));

  50. int res = 0;

  51. while (pos != 0) {

  52. mostRightOne = pos & (~pos + 1);

  53. pos = pos - mostRightOne;

  54. res += process2(upperLim, colLim | mostRightOne,

  55. (leftDiaLim | mostRightOne) << 1,

  56. (rightDiaLim | mostRightOne) >>> 1);

  57. }

  58. return res;

  59. }

  60.  
  61. public static void main(String[] args) {

  62. int n = 32;

  63.  
  64. long start = System.currentTimeMillis();

  65. System.out.println(num2(n));

  66. long end = System.currentTimeMillis();

  67. System.out.println("cost time: " + (end - start) + "ms");

  68.  
  69. start = System.currentTimeMillis();

  70. System.out.println(num1(n));

  71. end = System.currentTimeMillis();

  72. System.out.println("cost time: " + (end - start) + "ms");

  73. }

  74. }

二叉搜索树实现

本文给出二叉搜索树介绍和实现

 

首先说它的性质:所有的节点都满足,左子树上所有的节点都比自己小,右边的都比自己大。

 

那这个结构有什么有用呢?

首先可以快速二分查找。还可以中序遍历得到升序序列,等等。。。

基本操作:

1、插入某个数值

2、查询是否包含某个数值

3、删除某个数值

 

根据实现不同,还可以实现其他很多种操作。

 

实现思路思路:

前两个操作很好想,就是不断比较,大了往左走,小了往右走。到空了插入,或者到空都没找到。

而删除稍微复杂一些,有下面这几种情况:

1、需要删除的节点没有左儿子,那就把右儿子提上去就好了。

2、需要删除的节点有左儿子,这个左儿子没有右儿子,那么就把左儿子提上去

3、以上都不满足,就把左儿子子孙中最大节点提上来。

 

当然,反过来也是成立的,比如右儿子子孙中最小的节点。

 

下面来叙述为什么可以这么做。

下图中A为待删除节点。

第一种情况:

 

1、去掉A,把c提上来,c也是小于x的没问题。

2、根据定义可知,x左边的所有点都小于它,把c提上来不影响规则。

 

第二种情况

 

3、B<A<C,所以B<C,根据刚才的叙述,B可以提上去,c可以放在b右边,不影响规则

4、同理

 

第三种情况

 

5、注意:是把黑色的提升上来,不是所谓的最右边的那个,因为当初向左拐了,他一定小。

因为黑色是最大,比B以及B所有的孩子都大,所以让B当左孩子没问题

而黑点小于A,也就小于c,所以可以让c当右孩子

大概证明就这样。。

下面我们用代码实现并通过注释理解

上次链表之类的用的c,循环来写的。这次就c++函数递归吧,不同方式练习。

定义

 
  1. struct node

  2. {

  3. int val;//数据

  4. node *lch,*rch;//左右孩子

  5. };

插入

 
  1. node *insert(node *p,int x)

  2. {

  3. if(p==NULL)//直到空就创建节点

  4. {

  5. node *q=new node;

  6. q->val=x;

  7. q->lch=q->rch=NULL;

  8. return p;

  9. }

  10. if(x<p->val)p->lch=insert(p->lch,x);

  11. else p->lch=insert(p->rch,x);

  12. return p;//依次返回自己,让上一个函数执行。

  13. }

查找

 
  1. bool find(node *p,int x)

  2. {

  3. if(p==NULL)return false;

  4. else if(x==p->val)return true;

  5. else if(x<p->val)return find(p->lch,x);

  6. else return find(p->rch,x);

  7. }

删除

 
  1. node *remove(node *p,int x)

  2. {

  3. if(p==NULL)return NULL;

  4. else if(x<p->val)p->lch=remove(p->lch,x);

  5. else if(x>p->val)p->lch=remove(p->rch,x);

  6. //以下为找到了之后

  7. else if(p->lch==NULL)//情况1

  8. {

  9. node *q=p->rch;

  10. delete p;

  11. return q;

  12. }

  13. else if(p->lch->rch)//情况2

  14. {

  15. node *q=p->lch;

  16. q->rch=p->rch;

  17. delete p;

  18. return q;

  19. }

  20. else

  21. {

  22. node *q;

  23. for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大节点的前一个

  24. node *r=q->rch;//最大节点

  25. q->rch=r->lch;//最大节点左孩子提到最大节点位置

  26. r->lch=p->lch;//调整黑点左孩子为B

  27. r->rch=p->rch;//调整黑点右孩子为c

  28. delete p;//删除

  29. return r;//返回给父

  30. }

  31. return p;

  32. }

Abstract Self-Balancing Binary Search Tree

 

二叉搜索树

 

二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
具体介绍和实现:https://blog.csdn.net/hebtu666/article/details/81741034

我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,

此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度

 

概念引入

 

Abstract Self-Balancing Binary Search Tree:自平衡二叉搜索树

顾名思义:它在面对任意节点插入和删除时自动保持其高度

常用算法有红黑树、AVL、Treap、伸展树、SB树等。在平衡二叉搜索树中,我们可以看到,其高度一般都良好地维持在O(log(n)),大大降低了操作的时间复杂度。这些结构为可变有序列表提供了有效的实现,并且可以用于其他抽象数据结构,例如关联数组优先级队列集合

对于这些结构,他们都有自己的平衡性,比如:

AVL树

具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

根据定义可知,这是根据深度最严苛的标准了,左右子树高度不能差的超过1.

具体介绍和实现:https://blog.csdn.net/hebtu666/article/details/85047648

 

红黑树

特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

根据定义,确保没有一条路径会比其他路径长出2倍。

 

size balance tree

Size Balanced Tree(简称SBT)是一自平衡二叉查找树,是在计算机科学中用到的一种数据结构。它是由中国广东中山纪念中学的陈启峰发明的。陈启峰于2006年底完成论文《Size Balanced Tree》,并在2007年的全国青少年信息学奥林匹克竞赛冬令营中发表。由于SBT的拼写很容易找到中文谐音,它常被中国的信息学竞赛选手和ACM/ICPC选手们戏称为“傻B树”、“Super BT”等。相比红黑树、AVL树等自平衡二叉查找树,SBT更易于实现。据陈启峰在论文中称,SBT是“目前为止速度最快的高级二叉搜索树”。SBT能在O(log n)的时间内完成所有二叉搜索树(BST)的相关操作,而与普通二叉搜索树相比,SBT仅仅加入了简洁的核心操作Maintain。由于SBT赖以保持平衡的是size域而不是其他“无用”的域,它可以很方便地实现动态顺序统计中的selectrank操作。

对于SBT的每一个结点 t,有如下性质:
   性质(a) s[ right[t] ]≥s[ left [ left[ t ] ] ], s[ right [ left[t] ] ]
   性质(b) s[ left[t] ]≥s[right[ right[t] ] ], s[ left[ right[t] ] ]
即.每棵子树的大小不小于其兄弟的子树大小。

 

伸展树

伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由Daniel Sleator和Robert Tarjan创造。它的优势在于不需要记录用于平衡树的冗余信息。在伸展树上的一般操作都基于伸展操作。

 

Treap

Treap是一棵二叉排序树,它的左子树和右子树分别是一个Treap,和一般的二叉排序树不同的是,Treap纪录一个额外的数据,就是优先级。Treap在以关键码构成二叉排序树的同时,还满足的性质(在这里我们假设节点的优先级大于该节点的孩子的优先级)。但是这里要注意的是Treap二叉堆有一点不同,就是二叉堆必须是完全二叉树,而Treap并不一定是。

 

 

 

 

对比可以发现,AVL树对平衡性的要求比较严苛,每插入一个节点就很大概率面临调整。

而红黑树对平衡性的要求没有那么严苛。可能是多次插入攒够了一下调整。。。

 

把每一个树的细节都扣清楚是一件挺无聊的事。。虽然据说红黑树都成了面试必问内容,但是实在是不想深究那些细节,这些树的基本操作也无非是那么两种:左旋,右旋。这些树的所有操作和情况,都是这两种动作的组合罢了。

所以本文先介绍这两种基本操作,等以后有时间(可能到找工作时),再把红黑树等结构的细节补上。

 

最简单的旋转

 

最简单的例子:

这棵树,左子树深度为2,右子树深度为0,所以,根据AVL树或者红黑树的标准,它都不平衡。。

那怎么办?转过来:

是不是就平衡了?

这就是我们的顺时针旋转,又叫,右旋,因为是以2为轴,把1转下来了。

左旋同理。

 

带子树旋转

问题是,真正转起来可没有这么简单:

这才是一颗搜索树的样子啊

ABCD都代表是一颗子树。我们这三个点转了可不能不管这些子树啊对不对。

好,我们想想这些子树怎么办。

首先,AB子树没有关系,放在原地即可。

D作为3的右子树,也可以不动,那剩下一个位置,会不会就是放C子树呢?

我们想想能否这样做。

原来:

1)C作为2的右子树,内任何元素都比2大。

2)C作为3左子树的一部分,内任何元素都比3小。

转之后:

1)C作为2的右子树的一部分,内任何元素都比2大。

2)C作为3左子树,内任何元素都比3小。

所以,C子树可以作为3的左子树,没有问题。

这样,我们的操作就介绍完了。

这种基本的变换达到了看似把树变的平衡的效果。

左右旋转类似

 

代码实现

对于Abstract BinarySearchTree类,上面网址已经给出了思路和c++代码实现,把java再贴出来也挺无趣的,所以希望大家能自己实现。

抽象自平衡二叉搜索树(AbstractSelfBalancingBinarySearchTree)的所有操作都是建立在二叉搜索树(BinarySearchTree )操作的基础上来进行的。

各种自平衡二叉搜索树(AVL、红黑树等)的操作也是由Abstract自平衡二叉搜索树的基本操作:左旋、右旋构成。这个文章只写了左旋右旋基本操作,供以后各种selfBalancingBinarySearchTree使用。

 
  1. public abstract class AbstractSelfBalancingBinarySearchTree extends AbstractBinarySearchTree {

  2. protected Node rotateRight(Node node) {

  3. Node temp = node.left;//节点2

  4. temp.parent = node.parent;

  5. //节点3的父(旋转后节点2的父)

  6. node.left = temp.right;

  7. //节点3接收节点2的右子树

  8. if (node.left != null) {

  9. node.left.parent = node;

  10. }

  11.  
  12. temp.right = node;

  13. //节点3变为节点2的右孩子

  14. node.parent = temp;

  15.  
  16. //原来节点3的父(若存在),孩子变为节点2

  17. if (temp.parent != null) {

  18. if (node == temp.parent.left) {

  19. temp.parent.left = temp;

  20. } else {

  21. temp.parent.right = temp;

  22. }

  23. } else {

  24. root = temp;

  25. }

  26. return temp;

  27. }

  28.  
  29. protected Node rotateLeft(Node node) {

  30. Node temp = node.right;

  31. temp.parent = node.parent;

  32. node.right = temp.left;

  33. if (node.right != null) {

  34. node.right.parent = node;

  35. }

  36. temp.left = node;

  37. node.parent = temp;

  38. if (temp.parent != null) {

  39. if (node == temp.parent.left) {

  40. temp.parent.left = temp;

  41. } else {

  42. temp.parent.right = temp;

  43. }

  44. } else {

  45. root = temp;

  46. }

  47.  
  48. return temp;

  49. }

  50. }

 

AVL Tree

 

前言

 

希望读者

了解二叉搜索树

了解左旋右旋基本操作

https://blog.csdn.net/hebtu666/article/details/84992363

直观感受直接到文章底部,有正确的调整策略动画,自行操作。

二叉搜索树
 

二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
具体介绍和实现:https://blog.csdn.net/hebtu666/article/details/81741034

我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,

此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。
 

AVL Tree

计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。

这种结构是对平衡性要求最严苛的self-Balancing Binary Search Tree。

旋转操作继承自self-Balancing Binary Search Tree

public class AVLTree extends AbstractSelfBalancingBinarySearchTree

旋转

上面网址中已经介绍了二叉搜索树的调整和自平衡二叉搜索树的基本操作(左旋右旋),上篇文章我是这样定义左旋的:

达到了   看似   更平衡的效果。

我们回忆一下:

看起来好像不是很平,对吗?我们转一下:

看起来平了很多。

但!是!

只是看起来而已。

我们知道。ABCD其实都是子树,他们也有自己的深度,如果是这种情况:

我们简化一下:

转之后(A上来,3作为A的右孩子,A的右子树作为新的3的左孩子):

没错,旋转确实让树变平衡了,这是因为,不平衡是由A的左子树造成的,A的左子树深度更深。

我们这样旋转实际上是让

A的左子树相对于B提上去了两层,深度相对于B,-2,

A的右子树相对于B提上去了一层,深度相对于B,-1.

而如果是这样的:

旋转以后:

依旧是不平的。

那我们怎么解决这个问题呢?

先3的左子树旋转:

细节问题:不再讲解

这样,我们的最深处又成了左子树的左子树。然后再按原来旋转就好了。

 

旋转总结

 

那我们来总结一下旋转策略:

单向右旋平衡处理LL:

由于在*a的左子树根结点的左子树上插入结点,*a的平衡因子由1增至2,致使以*a为根的子树失去平衡,则需进行一次右旋转操作;

单向左旋平衡处理RR:

由于在*a的右子树根结点的右子树上插入结点,*a的平衡因子由-1变为-2,致使以*a为根的子树失去平衡,则需进行一次左旋转操作;

双向旋转(先左后右)平衡处理LR:

由于在*a的左子树根结点的右子树上插入结点,*a的平衡因子由1增至2,致使以*a为根的子树失去平衡,则需进行两次旋转(先左旋后右旋)操作。

双向旋转(先右后左)平衡处理RL:

由于在*a的右子树根结点的左子树上插入结点,*a的平衡因子由-1变为-2,致使以*a为根的子树失去平衡,则需进行两次旋转(先右旋后左旋)操作。

 

深度的记录

 

我们解决了调整问题,但是我们怎么发现树不平衡呢?总不能没插入删除一次都遍历一下求深度吧。

当然要记录一下了。

我们需要知道左子树深度和右子树深度。这样,我们可以添加两个变量,记录左右子树的深度。

但其实不需要,只要记录自己的深度即可。然后左右子树深度就去左右孩子去寻找即可。

这样就引出了一个问题:深度的修改、更新策略是什么呢?

单个节点的深度更新

本棵树的深度=(左子树深度,右子树深度)+1

所以写出节点node的深度更新方法:

 
  1. private static final void updateHeight(AVLNode node) {

  2. //不存在孩子,为-1,最后+1,深度为0

  3. int leftHeight = (node.left == null) ? -1 : ((AVLNode) node.left).height;

  4. int rightHeight = (node.right == null) ? -1 : ((AVLNode) node.right).height;

  5. node.height = 1 + Math.max(leftHeight, rightHeight);

  6. }

 

写出旋转代码

配合上面的方法和文章头部给出文章Abstract Self-Balancing Binary Search Tree的旋转,我们可以AVL树的四种旋转:

 
  1. private Node avlRotateLeft(Node node) {

  2. Node temp = super.rotateLeft(node);

  3. updateHeight((AVLNode)temp.left);

  4. updateHeight((AVLNode)temp);

  5. return temp;

  6. }

  7.  
  8. private Node avlRotateRight(Node node) {

  9. Node temp = super.rotateRight(node);

  10. updateHeight((AVLNode)temp.right);

  11. updateHeight((AVLNode)temp);

  12. return temp;

  13. }

  14.  
  15. protected Node doubleRotateRightLeft(Node node) {

  16. node.right = avlRotateRight(node.right);

  17. return avlRotateLeft(node);

  18. }

  19.  
  20. protected Node doubleRotateLeftRight(Node node) {

  21. node.left = avlRotateLeft(node.left);

  22. return avlRotateRight(node);

  23. }

请自行模拟哪些节点的深度记录需要修改。

 

总写调整方法

 

我们写出了旋转的操作和相应的深度更新。

现在我们把这些方法分情况总写。

 
  1. private void rebalance(AVLNode node) {

  2. while (node != null) {

  3. Node parent = node.parent;

  4.  
  5. int leftHeight = (node.left == null) ? -1 : ((AVLNode) node.left).height;

  6. int rightHeight = (node.right == null) ? -1 : ((AVLNode) node.right).height;

  7. int nodeBalance = rightHeight - leftHeight;

  8.  
  9. if (nodeBalance == 2) {

  10. if (((AVLNode)node.right.right).height+1 == rightHeight) {

  11. node = (AVLNode)avlRotateLeft(node);

  12. break;

  13. } else {

  14. node = (AVLNode)doubleRotateRightLeft(node);

  15. break;

  16. }

  17. } else if (nodeBalance == -2) {

  18. if (((AVLNode)node.left.left).height+1 == leftHeight) {

  19. node = (AVLNode)avlRotateRight(node);

  20. break;

  21. } else {

  22. node = (AVLNode)doubleRotateLeftRight(node);

  23. break;

  24. }

  25. } else {

  26. updateHeight(node);//平衡就一直往上更新高度

  27. }

  28.  
  29. node = (AVLNode)parent;

  30. }

  31. }

插入完工

 

我们的插入就完工了。

 
  1. public Node insert(int element) {

  2. Node newNode = super.insert(element);//插入

  3. rebalance((AVLNode)newNode);//调整

  4. return newNode;

  5. }

 

删除

也是一样的思路,自底向上,先一路修改高度后,进行rebalance调整。

 
  1. public Node delete(int element) {

  2. Node deleteNode = super.search(element);

  3. if (deleteNode != null) {

  4. Node successorNode = super.delete(deleteNode);

  5. //结合上面网址二叉搜索树实现的情况介绍

  6. if (successorNode != null) {

  7. // if replaced from getMinimum(deleteNode.right)

  8. // then come back there and update heights

  9. AVLNode minimum = successorNode.right != null ? (AVLNode)getMinimum(successorNode.right) : (AVLNode)successorNode;

  10. recomputeHeight(minimum);

  11. rebalance((AVLNode)minimum);

  12. } else {

  13. recomputeHeight((AVLNode)deleteNode.parent);//先修改

  14. rebalance((AVLNode)deleteNode.parent);//再调整

  15. }

  16. return successorNode;

  17. }

  18. return null;

  19. }

  20. /**

  21. * Recomputes height information from the node and up for all of parents. It needs to be done after delete.

  22. */

  23. private void recomputeHeight(AVLNode node) {

  24. while (node != null) {

  25. node.height = maxHeight((AVLNode)node.left, (AVLNode)node.right) + 1;

  26. node = (AVLNode)node.parent;

  27. }

  28. }

  29.  
  30. /**

  31. * Returns higher height of 2 nodes.

  32. */

  33. private int maxHeight(AVLNode node1, AVLNode node2) {

  34. if (node1 != null && node2 != null) {

  35. return node1.height > node2.height ? node1.height : node2.height;

  36. } else if (node1 == null) {

  37. return node2 != null ? node2.height : -1;

  38. } else if (node2 == null) {

  39. return node1 != null ? node1.height : -1;

  40. }

  41. return -1;

  42. }

请手动模拟哪里的高度需要改,哪里不需要改。

 

直观表现程序

 

如果看的比较晕,或者直接从头跳下来的同学,这个程序是正确的模拟了,维护AVL树的策略和一些我没写的基本操作。大家可以自己操作,直观感受一下。

https://www.cs.usfca.edu/~galles/visualization/AVLtree.html?utm_source=qq&utm_medium=social&utm_oi=826801573962338304

 

跳表介绍和实现

 

想慢慢的给大家自然的引入跳表。

 

想想,我们

1)在有序数列里搜索一个数

2)或者把一个数插入到正确的位置

都怎么做?

很简单吧

对于第一个操作,我们可以一个一个比较,在数组中我们可以二分,这样比链表快

对于第二个操作,二分也没什么用,因为找到位置还要在数组中一个一个挪位置,时间复杂度依旧是o(n)。

那我们怎么发明一个查找插入都比较快的结构呢?

 

 

 

可以打一些标记:

这样我们把标记连起来,搜索一个数时先从标记开始搜起下一个标记比本身大的话就往下走,因为再往前就肯定不符合要求了。

比如我们要搜索18:

因为一次可以跨越好多数呀,自然快了一些。

既然可以打标记,我们可以改进一下,选出一些数来再打一层标记:

这样我们搜索20是这样的:

最终我们可以打好多层标记,我们从最高层开始搜索,一次可以跳过大量的数(依旧是右边大了就往下走)。

比如搜索26:

最好的情况,就是每一层的标记都减少一半,这样到了顶层往下搜索,其实和二分就没什么两样,我们最底层用链表串起来,插入一个元素也不需要移动元素,所谓跳表就完成了一大半了。

 

现在的问题是,我们对于一个新数,到底应该给它打几层标记呢?

(刚开始一个数都没有,所以解决了这个问题,我们一直用这个策略更新即可)

答案是。。。。。投硬币,全看脸。

我其实有点惊讶,我以为会有某些很强的和数学相关的算法,可以保证一个很好的搜索效率,是我想多了。

我们对于一个新数字,有一半概率可以打一层标记,有一半概率不可以打。

对于打了一层标记的数,我们依旧是这个方法,它有一半概率再向上打一层标记,依次循环。

所以每一层能到达的概率都少一半。

各层的节点数量竟然就可以比较好的维护在很好的效率上(最完美的就是达到了二分的效果)

 

再分析一下,其实对于同一个数字:

等等。。

其实没必要全都用指针,因为我们知道,通过指针找到一个数可比下标慢多了。

所以同一个数字的所有标记,没必要再用指针,效率低还不好维护,用一个list保存即可。

这样,我们就设计出来一个数字的所有标记组成的结构:

 
  1. public static class SkipListNode {

  2. public Integer value;//本身的值

  3. public ArrayList<SkipListNode> nextNodes;

  4. //指向下一个元素的结点组成的数组,长度全看脸。

  5.  
  6. public SkipListNode(Integer value) {

  7. this.value = value;

  8. nextNodes = new ArrayList<SkipListNode>();

  9. }

  10. }

将integer比较的操作封装一下:

 
  1. private boolean lessThan(Integer a, Integer b) {

  2. return a.compareTo(b) < 0;

  3. }

  4.  
  5. private boolean equalTo(Integer a, Integer b) {

  6. return a.compareTo(b) == 0;

  7. }

找到在本层应该往下拐的结点:

  1. // Returns the node at a given level with highest value less than e

  2. private SkipListNode findNext(Integer e, SkipListNode current, int level) {

  3. SkipListNode next = current.nextNodes.get(level);

  4. while (next != null) {

  5. Integer value = next.value;

  6. if (lessThan(e, value)) { // e < value

  7. break;

  8. }

  9. current = next;

  10. next = current.nextNodes.get(level);

  11. }

  12. return current;

  13. }

这样我们就写一个一层层往下找的方法,并且封装成find(Integer e)的形式:

 
  1. // Returns the skiplist node with greatest value <= e

  2. private SkipListNode find(Integer e) {

  3. return find(e, head, maxLevel);

  4. }

  5.  
  6. // Returns the skiplist node with greatest value <= e

  7. // Starts at node start and level

  8. private SkipListNode find(Integer e, SkipListNode current, int level) {

  9. do {

  10. current = findNext(e, current, level);

  11. } while (level-- > 0);

  12. return current;

  13. }

刚才的方法是找到最大的小于等于目标的值,如果找到的值等于目标,跳表中就存在这个目标。否则不存在。

 
  1. public boolean contains(Integer value) {

  2. SkipListNode node = find(value);

  3. return node != null && node.value != null && equalTo(node.value, value);

  4. }

我们现在可以实现加入一个新点了,要注意把每层的标记打好:

 
  1. public void add(Integer newValue) {

  2. if (!contains(newValue)) {

  3. size++;

  4. int level = 0;

  5. while (Math.random() < PROBABILITY) {

  6. level++;//能有几层全看脸

  7. }

  8. while (level > maxLevel) {//大于当前最大层数

  9. head.nextNodes.add(null);//直接连系统最大

  10. maxLevel++;

  11. }

  12. SkipListNode newNode = new SkipListNode(newValue);

  13. SkipListNode current = head;//前一个结点,也就是说目标应插current之后

  14. do {//每一层往下走之前就可以设置这一层的标记了,就是链表插入一个新节点

  15. current = findNext(newValue, current, level);

  16. newNode.nextNodes.add(0, current.nextNodes.get(level));

  17. current.nextNodes.set(level, newNode);

  18. } while (level-- > 0);

  19. }

  20. }

删除也是一样的

 
  1. public void delete(Integer deleteValue) {

  2. if (contains(deleteValue)) {

  3. SkipListNode deleteNode = find(deleteValue);

  4. size--;

  5. int level = maxLevel;

  6. SkipListNode current = head;

  7. do {//就是一个链表删除节点的操作

  8. current = findNext(deleteNode.value, current, level);

  9. if (deleteNode.nextNodes.size() > level) {

  10. current.nextNodes.set(level, deleteNode.nextNodes.get(level));

  11. }

  12. } while (level-- > 0);

  13. }

  14. }

作为一个容器,Iterator那是必须有的吧,里面肯定有hasNext和next吧?

 
  1. public static class SkipListIterator implements Iterator<Integer> {

  2. SkipList list;

  3. SkipListNode current;

  4.  
  5. public SkipListIterator(SkipList list) {

  6. this.list = list;

  7. this.current = list.getHead();

  8. }

  9.  
  10. public boolean hasNext() {

  11. return current.nextNodes.get(0) != null;

  12. }

  13.  
  14. public Integer next() {

  15. current = current.nextNodes.get(0);

  16. return current.value;

  17. }

  18. }

这个跳表我们就实现完了。

现实工作中呢,我们一般不会让它到无限多层,万一有一个数它人气爆炸随机数冲到了一万层呢?

所以包括redis在内的一些跳表实现,都是规定了一个最大层数的。

别的好像也没什么了。

最后贴出所有代码。

 
  1. import java.util.ArrayList;

  2. import java.util.Iterator;

  3.  
  4. public SkipListDemo {

  5.  
  6. public static class SkipListNode {

  7. public Integer value;

  8. public ArrayList<SkipListNode> nextNodes;

  9.  
  10. public SkipListNode(Integer value) {

  11. this.value = value;

  12. nextNodes = new ArrayList<SkipListNode>();

  13. }

  14. }

  15.  
  16. public static class SkipListIterator implements Iterator<Integer> {

  17. SkipList list;

  18. SkipListNode current;

  19.  
  20. public SkipListIterator(SkipList list) {

  21. this.list = list;

  22. this.current = list.getHead();

  23. }

  24.  
  25. public boolean hasNext() {

  26. return current.nextNodes.get(0) != null;

  27. }

  28.  
  29. public Integer next() {

  30. current = current.nextNodes.get(0);

  31. return current.value;

  32. }

  33. }

  34.  
  35. public static class SkipList {

  36. private SkipListNode head;

  37. private int maxLevel;

  38. private int size;

  39. private static final double PROBABILITY = 0.5;

  40.  
  41. public SkipList() {

  42. size = 0;

  43. maxLevel = 0;

  44. head = new SkipListNode(null);

  45. head.nextNodes.add(null);

  46. }

  47.  
  48. public SkipListNode getHead() {

  49. return head;

  50. }

  51.  
  52. public void add(Integer newValue) {

  53. if (!contains(newValue)) {

  54. size++;

  55. int level = 0;

  56. while (Math.random() < PROBABILITY) {

  57. level++;

  58. }

  59. while (level > maxLevel) {

  60. head.nextNodes.add(null);

  61. maxLevel++;

  62. }

  63. SkipListNode newNode = new SkipListNode(newValue);

  64. SkipListNode current = head;

  65. do {

  66. current = findNext(newValue, current, level);

  67. newNode.nextNodes.add(0, current.nextNodes.get(level));

  68. current.nextNodes.set(level, newNode);

  69. } while (level-- > 0);

  70. }

  71. }

  72.  
  73. public void delete(Integer deleteValue) {

  74. if (contains(deleteValue)) {

  75. SkipListNode deleteNode = find(deleteValue);

  76. size--;

  77. int level = maxLevel;

  78. SkipListNode current = head;

  79. do {

  80. current = findNext(deleteNode.value, current, level);

  81. if (deleteNode.nextNodes.size() > level) {

  82. current.nextNodes.set(level, deleteNode.nextNodes.get(level));

  83. }

  84. } while (level-- > 0);

  85. }

  86. }

  87.  
  88. // Returns the skiplist node with greatest value <= e

  89. private SkipListNode find(Integer e) {

  90. return find(e, head, maxLevel);

  91. }

  92.  
  93. // Returns the skiplist node with greatest value <= e

  94. // Starts at node start and level

  95. private SkipListNode find(Integer e, SkipListNode current, int level) {

  96. do {

  97. current = findNext(e, current, level);

  98. } while (level-- > 0);

  99. return current;

  100. }

  101.  
  102. // Returns the node at a given level with highest value less than e

  103. private SkipListNode findNext(Integer e, SkipListNode current, int level) {

  104. SkipListNode next = current.nextNodes.get(level);

  105. while (next != null) {

  106. Integer value = next.value;

  107. if (lessThan(e, value)) { // e < value

  108. break;

  109. }

  110. current = next;

  111. next = current.nextNodes.get(level);

  112. }

  113. return current;

  114. }

  115.  
  116. public int size() {

  117. return size;

  118. }

  119.  
  120. public boolean contains(Integer value) {

  121. SkipListNode node = find(value);

  122. return node != null && node.value != null && equalTo(node.value, value);

  123. }

  124.  
  125. public Iterator<Integer> iterator() {

  126. return new SkipListIterator(this);

  127. }

  128.  
  129. /******************************************************************************

  130. * Utility Functions *

  131. ******************************************************************************/

  132.  
  133. private boolean lessThan(Integer a, Integer b) {

  134. return a.compareTo(b) < 0;

  135. }

  136.  
  137. private boolean equalTo(Integer a, Integer b) {

  138. return a.compareTo(b) == 0;

  139. }

  140.  
  141. }

  142.  
  143. public static void main(String[] args) {

  144.  
  145. }

  146.  
  147. }

  148.  

c语言实现排序和查找所有算法

 

 c语言版排序查找完成,带详细解释,一下看到爽,能直接运行看效果。

 

 
  1. /* Note:Your choice is C IDE */

  2. #include "stdio.h"

  3. #include"stdlib.h"

  4. #define MAX 10

  5. void SequenceSearch(int *fp,int Length);

  6. void Search(int *fp,int length);

  7. void Sort(int *fp,int length);

  8. /*

  9. 注意:

  10. 1、数组名x,*(x+i)就是x[i]哦

  11.  
  12. */

  13.  
  14.  
  15. /*

  16. ================================================

  17. 功能:选择排序

  18. 输入:数组名称(数组首地址)、数组中元素个数

  19. ================================================

  20. */

  21. void select_sort(int *x, int n)

  22. {

  23. int i, j, min, t;

  24. for (i=0; i<n-1; i++) /*要选择的次数:下标:0~n-2,共n-1次*/

  25. {

  26. min = i; /*假设当前下标为i的数最小,比较后再调整*/

  27. for (j=i+1; j<n; j++)/*循环找出最小的数的下标是哪个*/

  28. {

  29. if (*(x+j) < *(x+min))

  30. min = j; /*如果后面的数比前面的小,则记下它的下标*/

  31. }

  32. if (min != i) /*如果min在循环中改变了,就需要交换数据*/

  33. {

  34. t = *(x+i);

  35. *(x+i) = *(x+min);

  36. *(x+min) = t;

  37. }

  38. }

  39. }

  40. /*

  41. ================================================

  42. 功能:直接插入排序

  43. 输入:数组名称(也就是数组首地址)、数组中元素个数

  44. ================================================

  45. */

  46.  
  47. void insert_sort(int *x, int n)

  48. {

  49. int i, j, t;

  50. for (i=1; i<n; i++) /*要选择的次数:下标1~n-1,共n-1次*/

  51. {

  52. /*

  53. 暂存下标为i的数。注意:下标从1开始,原因就是开始时

  54. 第一个数即下标为0的数,前面没有任何数,认为它是排

  55. 好顺序的。

  56. */

  57. t=*(x+i);

  58. for (j=i-1; j>=0 && t<*(x+j); j--) /*注意:j=i-1,j--,这里就是下标为i的数,在它前面有序列中找插入位置。*/

  59. {

  60. *(x+j+1) = *(x+j); /*如果满足条件就往后挪。最坏的情况就是t比下标为0的数都小,它要放在最前面,j==-1,退出循环*/

  61. }

  62. *(x+j+1) = t; /*找到下标为i的数的放置位置*/

  63. }

  64. }

  65. /*

  66. ================================================

  67. 功能:冒泡排序

  68. 输入:数组名称(也就是数组首地址)、数组中元素个数

  69. ================================================

  70. */

  71. void bubble_sort0(int *x, int n)

  72. {

  73. int j, h, t;

  74. for (h=0; h<n-1; h++)/*循环n-1次*/

  75. {

  76. for (j=0; j<n-2-h; j++)/*每次做的操作类似*/

  77. {

  78. if (*(x+j) > *(x+j+1)) /*大的放在后面,小的放到前面*/

  79. {

  80. t = *(x+j);

  81. *(x+j) = *(x+j+1);

  82. *(x+j+1) = t; /*完成交换*/

  83. }

  84. }

  85. }

  86. }

  87. /*优化:记录最后下沉位置,之后的肯定有序*/

  88. void bubble_sort(int *x, int n)

  89. {

  90. int j, k, h, t;

  91. for (h=n-1; h>0; h=k) /*循环到没有比较范围*/

  92. {

  93. for (j=0, k=0; j<h; j++) /*每次预置k=0,循环扫描后更新k*/

  94. {

  95. if (*(x+j) > *(x+j+1)) /*大的放在后面,小的放到前面*/

  96. {

  97. t = *(x+j);

  98. *(x+j) = *(x+j+1);

  99. *(x+j+1) = t; /*完成交换*/

  100. k = j; /*保存最后下沉的位置。这样k后面的都是排序排好了的。*/

  101. }

  102. }

  103. }

  104. }

  105. /*

  106. ================================================

  107. 功能:希尔排序

  108. 输入:数组名称(也就是数组首地址)、数组中元素个数

  109. ================================================

  110. */

  111.  
  112. void shell_sort(int *x, int n)

  113. {

  114. int h, j, k, t;

  115. for (h=n/2; h>0; h=h/2) /*控制增量*/

  116. {

  117. for (j=h; j<n; j++) /*这个实际上就是上面的直接插入排序*/

  118. {

  119. t = *(x+j);

  120. for (k=j-h; (k>=0 && t<*(x+k)); k-=h)

  121. {

  122. *(x+k+h) = *(x+k);

  123. }

  124. *(x+k+h) = t;

  125. }

  126. }

  127. }

  128. /*

  129. ================================================

  130. 功能:快速排序

  131. 输入:数组名称(也就是数组首地址)、数组中起止元素的下标

  132. 注:自己画画

  133. ================================================

  134. */

  135.  
  136. void quick_sort(int *x, int low, int high)

  137. {

  138. int i, j, t;

  139. if (low < high) /*要排序的元素起止下标,保证小的放在左边,大的放在右边。这里以下标为low的元素(最左边)为基准点*/

  140. {

  141. i = low;

  142. j = high;

  143. t = *(x+low); /*暂存基准点的数*/

  144. while (i<j) /*循环扫描*/

  145. {

  146. while (i<j && *(x+j)>t) /*在右边的只要比基准点大仍放在右边*/

  147. {

  148. j--; /*前移一个位置*/

  149. }

  150. if (i<j)

  151. {

  152. *(x+i) = *(x+j); /*上面的循环退出:即出现比基准点小的数,替换基准点的数*/

  153. i++; /*后移一个位置,并以此为基准点*/

  154. }

  155. while (i<j && *(x+i)<=t) /*在左边的只要小于等于基准点仍放在左边*/

  156. {

  157. i++; /*后移一个位置*/

  158. }

  159. if (i<j)

  160. {

  161. *(x+j) = *(x+i); /*上面的循环退出:即出现比基准点大的数,放到右边*/

  162. j--; /*前移一个位置*/

  163. }

  164. }

  165. *(x+i) = t; /*一遍扫描完后,放到适当位置*/

  166. quick_sort(x,low,i-1); /*对基准点左边的数再执行快速排序*/

  167. quick_sort(x,i+1,high); /*对基准点右边的数再执行快速排序*/

  168. }

  169. }

  170. /*

  171. ================================================

  172. 功能:堆排序

  173. 输入:数组名称(也就是数组首地址)、数组中元素个数

  174. 注:画画

  175. ================================================

  176. */

  177. /*

  178. 功能:建堆

  179. 输入:数组名称(也就是数组首地址)、参与建堆元素的个数、从第几个元素开始

  180. */

  181. void sift(int *x, int n, int s)

  182. {

  183. int t, k, j;

  184. t = *(x+s); /*暂存开始元素*/

  185. k = s; /*开始元素下标*/

  186. j = 2*k + 1; /*左子树元素下标*/

  187. while (j<n)

  188. {

  189. if (j<n-1 && *(x+j) < *(x+j+1))/*判断是否存在右孩子,并且右孩子比左孩子大,成立,就把j换为右孩子*/

  190. {

  191. j++;

  192. }

  193. if (t<*(x+j)) /*调整*/

  194. {

  195. *(x+k) = *(x+j);

  196. k = j; /*调整后,开始元素也随之调整*/

  197. j = 2*k + 1;

  198. }

  199. else /*没有需要调整了,已经是个堆了,退出循环。*/

  200. {

  201. break;

  202. }

  203. }

  204. *(x+k) = t; /*开始元素放到它正确位置*/

  205. }

  206. /*

  207. 功能:堆排序

  208. 输入:数组名称(也就是数组首地址)、数组中元素个数

  209. 注:

  210. *

  211. * *

  212. * - * *

  213. * * *

  214. 建堆时,从从后往前第一个非叶子节点开始调整,也就是“-”符号的位置

  215. */

  216. void heap_sort(int *x, int n)

  217. {

  218. int i, k, t;

  219. //int *p;

  220. for (i=n/2-1; i>=0; i--)

  221. {

  222. sift(x,n,i); /*初始建堆*/

  223. }

  224. for (k=n-1; k>=1; k--)

  225. {

  226. t = *(x+0); /*堆顶放到最后*/

  227. *(x+0) = *(x+k);

  228. *(x+k) = t;

  229. sift(x,k,0); /*剩下的数再建堆*/

  230. }

  231. }

  232.  
  233.  
  234.  
  235.  
  236. // 归并排序中的合并算法

  237. void Merge(int a[], int start, int mid, int end)

  238. {

  239. int i,k,j, temp1[10], temp2[10];

  240. int n1, n2;

  241. n1 = mid - start + 1;

  242. n2 = end - mid;

  243.  
  244. // 拷贝前半部分数组

  245. for ( i = 0; i < n1; i++)

  246. {

  247. temp1[i] = a[start + i];

  248. }

  249. // 拷贝后半部分数组

  250. for (i = 0; i < n2; i++)

  251. {

  252. temp2[i] = a[mid + i + 1];

  253. }

  254. // 把后面的元素设置的很大

  255. temp1[n1] = temp2[n2] = 1000;

  256. // 合并temp1和temp2

  257. for ( k = start, i = 0, j = 0; k <= end; k++)

  258. {

  259. //小的放到有顺序的数组里

  260. if (temp1[i] <= temp2[j])

  261. {

  262. a[k] = temp1[i];

  263. i++;

  264. }

  265. else

  266. {

  267. a[k] = temp2[j];

  268. j++;

  269. }

  270. }

  271. }

  272.  
  273. // 归并排序

  274. void MergeSort(int a[], int start, int end)

  275. {

  276. if (start < end)

  277. {

  278. int i;

  279. i = (end + start) / 2;

  280. // 对前半部分进行排序

  281. MergeSort(a, start, i);

  282. // 对后半部分进行排序

  283. MergeSort(a, i + 1, end);

  284. // 合并前后两部分

  285. Merge(a, start, i, end);

  286. }

  287. }

  288. /*顺序查找*/

  289. void SequenceSearch(int *fp,int Length)

  290. {

  291. int i;

  292. int data;

  293. printf("开始使用顺序查询.\n请输入你想要查找的数据.\n");

  294. scanf("%d",&data);

  295. for(i=0; i<Length; i++)

  296. if(fp[i]==data)

  297. {

  298. printf("经过%d次查找,查找到数据%d,表中位置为%d.\n",i+1,data,i);

  299. return ;

  300. }

  301. printf("经过%d次查找,未能查找到数据%d.\n",i,data);

  302. }

  303. /*二分查找*/

  304. void Search(int *fp,int Length)

  305. {

  306. int data;

  307. int bottom,top,middle;

  308. int i=0;

  309. printf("开始使用二分查询.\n请输入你想要查找的数据.\n");

  310. scanf("%d",&data);

  311. printf("由于二分查找法要求数据是有序的,现在开始为数组排序.\n");

  312. Sort(fp,Length);

  313. printf("数组现在已经是从小到大排列,下面将开始查找.\n");

  314. bottom=0;

  315. top=Length;

  316. while (bottom<=top)

  317. {

  318. middle=(bottom+top)/2;

  319. i++;

  320. if(fp[middle]<data)

  321. {

  322. bottom=middle+1;

  323. }

  324. else if(fp[middle]>data)

  325. {

  326. top=middle-1;

  327. }

  328. else

  329. {

  330. printf("经过%d次查找,查找到数据%d,在排序后的表中的位置为%d.\n",i,data,middle);

  331. return;

  332. }

  333. }

  334. printf("经过%d次查找,未能查找到数据%d.\n",i,data);

  335. }

  336.  
  337. /*

  338.  
  339. 下面测试了

  340.  
  341. */

  342. void Sort(int *fp,int Length)

  343. {

  344. int temp;

  345. int i,j,k;

  346. printf("现在开始为数组排序,排列结果将是从小到大.\n");

  347. for(i=0; i<Length; i++)

  348. for(j=0; j<Length-i-1; j++)

  349. if(fp[j]>fp[j+1])

  350. {

  351. temp=fp[j];

  352. fp[j]=fp[j+1];

  353. fp[j+1]=temp;

  354. }

  355. printf("排序完成!\n下面输出排序后的数组:\n");

  356. for(k=0; k<Length; k++)

  357. {

  358. printf("%5d",fp[k]);

  359. }

  360. printf("\n");

  361.  
  362. }

  363. /*构造随机输出函数类*/

  364. void input(int a[])

  365. {

  366. int i;

  367. srand( (unsigned int)time(NULL) );

  368. for (i = 0; i < 10; i++)

  369. {

  370. a[i] = rand() % 100;

  371. }

  372. printf("\n");

  373. }

  374. /*构造键盘输入函数类*/

  375. /*void input(int *p)

  376. {

  377. int i;

  378. printf("请输入 %d 个数据 :\n",MAX);

  379. for (i=0; i<MAX; i++)

  380. {

  381. scanf("%d",p++);

  382. }

  383. printf("\n");

  384. }*/

  385. /*构造输出函数类*/

  386. void output(int *p)

  387. {

  388. int i;

  389. for ( i=0; i<MAX; i++)

  390. {

  391. printf("%d ",*p++);

  392. }

  393. }

  394. void main()

  395. {

  396. int start=0,end=3;

  397. int *p, i, a[MAX];

  398. int count=MAX;

  399. int arr[MAX];

  400. int choise=0;

  401. /*printf("请输入你的数据的个数:\n");

  402. scanf("%d",&count);*/

  403. /* printf("请输入%d个数据\n",count);

  404. for(i=0;i<count;i++)

  405. {

  406. scanf("%d",&arr[i]);

  407. }*/

  408. /*录入测试数据*/

  409. input(a);

  410. printf("随机初始数组为:\n");

  411. output(a);

  412. printf("\n");

  413. do

  414. {

  415. printf("1.使用顺序查询.\n2.使用二分查找法查找.\n3.退出\n");

  416. scanf("%d",&choise);

  417. if(choise==1)

  418. SequenceSearch(a,count);

  419. else if(choise==2)

  420. Search(a,count);

  421. else if(choise==3)

  422. break;

  423. }

  424. while (choise==1||choise==2||choise==3);

  425.  
  426.  
  427. /*录入测试数据*/

  428. input(a);

  429. printf("随机初始数组为:\n");

  430. output(a);

  431. printf("\n");

  432. /*测试选择排序*/

  433. p = a;

  434. printf("选择排序之后的数据:\n");

  435. select_sort(p,MAX);

  436. output(a);

  437. printf("\n");

  438. system("pause");

  439. /**/

  440. /*录入测试数据*/

  441. input(a);

  442. printf("随机初始数组为:\n");

  443. output(a);

  444. printf("\n");

  445. /*测试直接插入排序*/

  446. printf("直接插入排序之后的数据:\n");

  447. p = a;

  448. insert_sort(p,MAX);

  449. output(a);

  450. printf("\n");

  451. system("pause");

  452. /*录入测试数据*/

  453. input(a);

  454. printf("随机初始数组为:\n");

  455. output(a);

  456. printf("\n");

  457. /*测试冒泡排序*/

  458. printf("冒泡排序之后的数据:\n");

  459. p = a;

  460. insert_sort(p,MAX);

  461. output(a);

  462. printf("\n");

  463. system("pause");

  464. /*录入测试数据*/

  465. input(a);

  466. printf("随机初始数组为:\n");

  467. output(a);

  468. printf("\n");

  469. /*测试快速排序*/

  470. printf("快速排序之后的数据:\n");

  471. p = a;

  472. quick_sort(p,0,MAX-1);

  473. output(a);

  474. printf("\n");

  475. system("pause");

  476. /*录入测试数据*/

  477. input(a);

  478. printf("随机初始数组为:\n");

  479. output(a);

  480. printf("\n");

  481. /*测试堆排序*/

  482. printf("堆排序之后的数据:\n");

  483. p = a;

  484. heap_sort(p,MAX);

  485. output(a);

  486. printf("\n");

  487. system("pause");

  488. /*录入测试数据*/

  489. input(a);

  490. printf("随机初始数组为:\n");

  491. output(a);

  492. printf("\n");

  493. /*测试归并排序*/

  494. printf("归并排序之后的数据:\n");

  495. p = a;

  496. MergeSort(a,start,end);

  497. output(a);

  498. printf("\n");

  499. system("pause");

  500. }

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值