first commit

This commit is contained in:
2021-07-05 11:27:03 +08:00
commit 74d27c8ca4
40 changed files with 4208 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
## 知识点汇总
1. 链表与数组。
2. 队列和栈,出栈与入栈。
3. 链表的删除、插入、反向。
4. 字符串操作。
5. Hash表的hash函数,冲突解决方法有哪些。
6. 各种排序:冒泡、选择、插入、希尔、归并、快排、堆排、桶排、基数的原理、平均时间复杂度、最坏时间复杂度、空间复杂度、是否稳定。
7. 快排的partition函数与归并的Merge函数。
8. 对冒泡与快排的改进。
9. 二分查找,与变种二分查找。
10. 二叉树、B+树、AVL树、红黑树、哈夫曼树。
11. 二叉树的前中后续遍历:递归与非递归写法,层序遍历算法。
12. 图的BFS与DFS算法,最小生成树prim算法与最短路径Dijkstra算法。
13. KMP算法。
14. 排列组合问题。
15. 动态规划、贪心算法、分治算法。(一般不会问到)
16. 大数据处理:类似10亿条数据找出最大的1000个数.........等等
+267
View File
@@ -0,0 +1,267 @@
## 插入排序
### 1.直接插入排序
**思想**:每次将一个待排序的数据按照其关键字的大小插入到前面已经排序好的数据中的适当位置,直到全部数据排序完成。
**时间复杂度**:O(n2) O(n) O(n2) (最坏 最好 平均)
**空间复杂度**:O(1)
**稳定性**: 稳定 每次都是在前面已排好序的序列中找到适当的位置,只有小的数字会往前插入,所以原来相同的两个数字在排序后相对位置不变。
代码:
/**
* 插入排序
* @param array
*/
public static void insertSort(int[] array) {
for (int i = 2; i < array.length; i++ ) {
int val = array[i];
int j = i -1;
while (j >= 0 && array[j] > val) { // array[j] > val
array[j+1] = array[j];
j--;
}
array[j+1] = val; // array[j+1] 不是array[j]
}
}
### 2.希尔排序
**思想**:希尔排序根据增量值对数据按下表进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整体采用直接插入排序得到有序数组,算法终止。
**时间复杂度**:O(n2) O(n) O(n1.5) (最坏,最好,平均)
**空间复杂度**:O(1)
**稳定性**:不稳定 因为是分组进行直接插入排序,原来相同的两个数字可能会被分到不同的组去,可能会使得后面的数字会排到前面,使得两个相同的数字排序前后位置发生变化。
**不稳定举例**: 4 3 3 2 按2为增量分组,则第二个3会跑到前面
**代码**:
public static void shellSort(int[] array) {
int len;
len = array.length / 2; // 分成n/2组
while (len >= 1) {
for (int i = len; i < array.length; ++i) { //对每组进行直接插入排序
int temp = array[i];
int j = i - len;
while (j >= 0 && array[j] > temp) {
array[j + len] = array[j];
j -= len;
}
array[j + len] = temp;
}
len /= 2;
}
}
## 交换排序
### 3.冒泡排序
**思想**:对待排序元素的关键字从后往前进行多遍扫描,遇到相邻两个关键字次序与排序规则不符时,就将这两个元素进行交换。这样关键字较小的那个元素就像一个泡泡一样,从最后面冒到最前面来。
**时间复杂度**:最坏:O(n2) 最好: O(n) 平均: O(n2)
**空间复杂度**:O(1)
**稳定性**:稳定,相邻的关键字两两比较,如果相等则不交换。所以排序前后的相等数字相对位置不变。
**代码**:
public static void bubbleSort(int[] array) {
boolean flag; // 用来判断当前这一轮是否有交换数值,若没有则表示已经排好许了
for (int i = 0; i < array.length; i++) {
flag = false;
/**
* 这边要注意 for (int j = array.length -1; j >= i + 1; j--)。 不要写成
* for (int j = i + 1; j < array.length ; j++)
*/
for (int j = array.length -1; j >= i + 1; j--) {
if (array[j -1 ] > array[j]) {
//数据交换
int temp = array[j - 1];
array[j - 1] = array[j];
array[j] = temp;
//设置标志位
flag = true;
}
}
if (!flag) {
break;
}
}
}
### 4.快速排序
**思想**:该算法是分治算法,首先选择一个基准元素,根据基准元素将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。基准元素的选择对快速排序的性能影响很大,所有一般会想打乱排序数组选择第一个元素或则随机地从后面选择一个元素替换第一个元素作为基准元素。
**时间复杂度**:最坏:O(n2) 最好: O(nlogn) 平均: O(nlogn)
**空间复杂度**:O(nlogn)用于方法栈
**稳定性**:不稳定 快排会将大于等于基准元素的关键词放在基准元素右边,加入数组 1 2 2 3 4 5 选择第二个2 作为基准元素,那么排序后 第一个2跑到了后面,相对位置发生变化。
**代码**:
public static void quickSort(int[] array) {
partition(array, 0, array.length - 1);
}
private static void partition(int[] array, int low, int high) {
if (low < high) {
int p = quickSort(array, low, high);
partition(array, low, p - 1);
partition(array, p + 1, high);
}
}
private static int quickSort(int[] array, int left, int right) {
Random random = new Random(System.currentTimeMillis());
int idx = random.nextInt(right - left + 1) + left; // 这边需要注意right - left + 1 (要加1)
exch(array, idx, left);
int val = array[left];
while (left < right) {
while (left < right && array[right] > val) {
right--;
}
if (left < right) {
array[left++] = array[right];
}
while (left < right && array[left] < val) {
left++;
}
if (left < right) {
array[right--] = array[left];
}
}
array[left] = val;
return left;
}
三向快速排序算法
/**
* 三向切分快速排序, 适用于存在大量重复元素的数组
*
* @param array
*/
public static void quick2waySort(int[] array) {
quick2waySort(array, 0, array.length - 1);
}
private static void quick2waySort(int[] array, int lo, int hi) {
if (hi <= lo) {
return;
}
int lt = lo, gt = hi, i = lo + 1;
int val = array[lo];
while (i <= gt) {
if (array[i] < val) {
exch(array, i++, lt++);
} else if (array[i] > val) {
exch(array, i, gt--);
} else {
i++;
}
}
// lt 到 gt 之间的都是等于val 的. 如果存在大量重复元素的数组使用该算法可以极大提升算法效率,
quick2waySort(array, lo, lt - 1);
quick2waySort(array, gt + 1, hi);
}
## 选择排序
### 5.直接选择排序
**思想**:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后每次从剩余未排序元素中继续寻找最小(大)元素放到已排序序列的末尾。以此类推,直到所有元素均排序完毕
**时间复杂度**:最坏:O(n2) 最好: O(n2) 平均: O(n2)
**空间复杂度**:O(1)
**稳定性**:不稳定 例如数组 2 2 1 3 第一次选择的时候把第一个2与1交换使得两个2的相对次序发生了改变。
**代码**:
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int minIdx = i;
for (int j = i + 1; j < array.length; j++) {
if (array[j] < array[minIdx]) {
minIdx = j;
}
}
exch(array, i, minIdx);
}
}
### 6.堆排序
**思想**:堆排序是利用堆的性质进行的一种选择排序,先将排序元素构建一个最大堆,每次堆中取出最大的元素并调整堆。将该取出的最大元素放到已排好序的序列前面。这种方法相对选择排序,时间复杂度更低,效率更高。
**时间复杂度**:最坏:O(nlog2n) 最好: O(nlog2n) 平均: O(nlog2n)
**空间复杂度**:O(1)
**稳定性**:不稳定 例如 5 10 15 10。 如果堆顶5先输出,则第三层的10(最后一个10)的跑到堆顶,然后堆稳定,继续输出堆顶,则刚才那个10跑到前面了,所以两个10排序前后的次序发生改变。
**代码**:
public static void heapSort(int[] array) {
int N = array.length -1;
for (int k = N / 2; k >= 1; k--) {
sink(array, k, N);
}
while (N > 1) {
// 最大堆, 选择最大值放在最后
exch(array, 1, N --);
sink(array, 1, N);
}
}
private static void sink(int[] array, int k, int N) {
while (2 * k <= N) {
int j = 2 * k;
if (j < N && array[j] < array[j+1]) { // <
j++;
}
if (array[j] < array[k]) break; // <
exch(array, k, j);
k = j;
}
}
## 7.归并排序
**思想**:归并排序采用了分治算法,首先递归将原始数组划分为若干子数组,对每个子数组进行排序。然后将排好序的子数组递归合并成一个有序的数组。
**时间复杂度**:最坏:O(nlog2n) 最好: O(nlog2n) 平均: O(nlog2n)
**空间复杂度**:O(n)
**稳定性**:稳定
**代码**:
public static void mergeSort(int[] array) {
sort(array, 0, array.length - 1);
}
private static void sort(int[] array, int left, int right) {
if (left < right) {
int middle = (left + right) >> 1;
//递归处理相关的合并事项
sort(array, left, middle);
sort(array, middle + 1, right);
merge(array, left, middle, right);
}
}
private static void merge(int[] array, int lo, int mid, int hi) {
//创建一个临时数组用来存储合并后的数据
int[] temp = new int[array.length];
int left = lo;
int right = mid + 1;
int k = lo;
while (left <= mid && right <= hi) {
if (array[left] < array[right])
temp[k++] = array[left++];
else
temp[k++] = array[right++];
}
//处理剩余未合并的部分
while (left <= mid) temp[k++] = array[left++];
while (right <= hi) temp[k++] = array[right++];
//将临时数组中的内容存储到原数组中
while (lo <= hi) array[lo] = temp[lo++];
}
## 8.基数排序算法
**思想**:基数排序是通过“分配”和“收集”过程来实现排序,首先根据数字的个位的数将数字放入0-9号桶中,然后将所有桶中所盛数据按照桶号由小到大,桶中由顶至底依次重新收集串起来,得到新的元素序列。然后递归对十位、百位这些高位采用同样的方式分配收集,直到没各位都完成分配收集得到一个有序的元素序列。
**时间复杂度**:最坏:O(d(r+n)) 最好:O(d(r+n)) 平均: O(d(r+n))
**空间复杂度**:O(dr+n) n个记录,d个关键码,关键码的取值范围为r
**稳定性**:稳定 基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
**代码**:
## 参考
http://blog.jobbole.com/90256/
个人公众号(欢迎关注):<br>
![](/assets/weix_gongzhonghao.jpg)
+393
View File
@@ -0,0 +1,393 @@
## 一.顺序查找
**1.1 思路**:这是最简单的算法,从头开始遍历每个元素,并将每个元素与查找元素比较,如果一致则返回。
**1.2 时间复杂度**: O(N)
**1.3 空间复杂度**: O(1)
**1.4 代码**:
public int search(int[] array, int num) {
if(array == null || array.length == 0) {
return -1;
}
for(int i = 0; i < array.length; i++) {
if (array[i] == num) {
return i;
}
}
return -1;
}
## 二.二分查找
**2.1 思路**:二分查找前提是查找的数组是有序的,利用数据有序的特性提高查找性能。首先与数组中间位置的值比较,如果查找值大于中间位置值,则对数组右边以相同的思路查找,否则在左边以相同方式查找。这种方式使得每次查找范围变为原来的1/2.
**2.2 时间复杂度**:O(log2n)
**2.3 空间复杂度**: O(1)
public int halfSearch(int[] array, int num) {
if(array == null || array.length == 0) {
return -1;
}
int lo = 0, hi = array.length-1;
while(lo <= hi) {
int mid = (lo + hi) >> 2;
if (array[mid] == num) {
return mid;
} else if (array[mid] < num) {
hi = mid -1;
} else {
lo = mid + 1;
}
}
return -1;
}
## 三. 变种二分查找
http://www.cr173.com/html/20428_1.html
## 四. hash 算法
**4.1 思想**:哈希表是根据设定的**哈希函数H(key)**和**处理冲突方法**将一组关键字映射到一个有限的地址区间上,并将关键字对应的值存储在该地址空间,可以通过关键字快速获取对应的值,这种表称为哈希表或散列,所得存储位置称为哈希地址或散列地址。作为线性数据结构与表格和队列等相比,哈希表无疑是查找速度比较快的一种。
**4.2 查找复杂度:** O(1)
**4.3 哈希函数**:
1. **直接寻址法**:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a?key + b,其中a和b为常数(这种散列函数叫做自身函数)
2. **数字分析法**:因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。
3. **平方取中法**:取关键字平方后的中间几位作为散列地址
4. **折叠法**:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
5. **除留余数法**:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。
**4.4 hash冲突及解决**
hash冲突在所难免,解决冲突是一个复杂问题。冲突主要取决于:
(1)与散列函数有关,一个好的散列函数的值应尽可能平均分布。
(2)与解决冲突的哈希冲突函数有关。
(3)与负载因子的大小。太大不一定就好,而且浪费空间严重,负载因子和散列函数是联动的。
解决冲突的办法:
(1)**开放定址法**:线性探查法、平方探查法、伪随机序列法、双哈希函数法。
(2) **拉链法**:把所有同义词,即hash值相同的记录,用单链表连接起来。
**4.5 应用:**
1.字符串哈希
2.加密哈希
3.几何哈希
4.布隆过滤器
**4.6 不足:**获取有序序列复杂度高
参考:
http://www.tuicool.com/articles/RnErui
## 五.二叉树搜索树
#### 5.1 思想
二叉查找树(Binary Search Tree),也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:
1.若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2.若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3.任意节点的左、右子树也分别为二叉查找树。
4.没有键值相等的节点(no duplicate nodes)。
复杂度: 插入和查找的时间复杂度均为O(logN), 最坏为O(N)
#### 5.2 插入
1. 如果当前结点是null,则创建新结点返回。
2. 如果插入结点比当前结点值大,则插入其右孩子结点中。
3. 如果插入结点比当前结点值小,则插入其左孩子结点中。
**复杂度**: 平均 O(logn) 最坏O(n)
**代码:**
public Tree insert(Tree root, int val) {
if (root == null) {
return new Tree(val);
}
if (val == root.val) {
return root;
} else if (val > root.val) {
root.right = insert(root.right, val);
} else {
root.left = insert(root.left, val);
}
return root;
}
### 5.3 查找
#### 5.3.1 某个值
**思想**:查找方式有点类型二分查找方法,知识这里采用的树结构进行存储。首先与根结点进行判断:
1. 如果当前结点为null,则直接放回Null。
2. 如果当前结点值与查找值相同则返回当前结点.
3. 如果当前结点值小余查找值,则递归地到当前结点的右孩子查找
4. 如果当前结点值大于查找值,则递归地到当前结点的左孩子查找。
**时间复杂度**: 平均O(logn) 最坏O(n)
public Tree search(Tree root, int val) {
if(root == null) {
return null;
}
if(root.val == val) {
return root;
} else if(root.val > val) {
return search(root.left, val);
} else {
return search(root.right, val);
}
}
####5.3.2 查找最小值
**思想**:根据二叉搜索树的特点,最小结点都是在最左结点上或者如果根结点无左孩子便是其本身.
public Tree min(Tree root) {
if(root == null) {
return null;
}
if (root.left != null) {
return min(root.left);
}
return root;
}
#### 5.3.2 查找最大结点
**思想**: 同最小结点。
public Tree max(Tree root) {
if(root == null) {
return null;
}
if (root.right != null) {
return max(root.right);
}
return root;
}
### 5.4 删除
#### 5.4.1 删除最小结点
**思想:**找到根结点最左结点,如果其不存在右孩子则直接删除,否则用右孩子替换最左结点。需要注意的是根结点可能为null和不存在做孩子的情况。
public Tree deleteMin(Tree root) {
if(root == null) {
return null;
}
if(root.left != null) {
root.left = deleteMin(root.left);
} else if (root.right != null) {
return root.right;
}
return null;
}
### 5.4.2 删除最大结点
**思想:** 与删除最小结点类型,根据二叉搜索树的特性,最大结点是根结点的最右孩子。所以只要找到最右孩子结点,其存在左结点的话就用左结点替换否则直接删除.
public Tree deleteMax(Tree root) {
if(root == null) {
return null;
}
if(root.right != null) {
root.right = deleteMax(root.right);
} else if(root.left != null) {
return root.left;
}
return null;
}
### 5.4.3 删除某个结点
**思想:**要删除一个结点首先需要找到该结点的位置,采用上面的查找方式进行查找。找到结点后就是删除的问题的,可以按照下面的策略进行删除。
1. 如果一个结点无做孩子和右孩子,那么就可以直接删除
2. 如果只存在一个孩子结点,则用孩子结点替换。
3. 如果存在两个孩子结点,那么可以用其左孩子最大的结点或右孩子最小结点替换,并删除最左孩子结点或最右孩子结点.
public Tree delete(Tree root, int val) {
if(root == null) {
return null;
}
if(root.val == val) {
if(root.left == null && root.right == null) {
return null;
} else if(root.left!= null && root.right != null) {
Tree leftBig = max(root.left);
root.val = leftBig.val;
root.left = delete(root.left, leftBig.val);
} else if(root.left != null){
return root.left;
} else {
return root.right;
}
} else if(root.val < val) {
root.right = delete(root.right, val);
} else {
root.left = delete(root.left, val);
}
return root;
}
参考: http://www.cnblogs.com/vamei/archive/2013/03/17/2962290.html
http://blog.jobbole.com/79305/
#### 5.5 相关的算法题
**剑指offer**:
## 6. AVL查找树
### 6.1 思想
二叉树查找树在插入时没有对二叉树的深度和结构做一个调整,使得叶子结点深度不一,在查找时深度越深的结点时间复杂度越高。为了改进查找的时间时间复杂度,于是出现了平衡二叉树(AVL).**平衡二叉树使得每个结点的左结点和右结点的深度差不超过1.**
### 6.2 查找
查找与二叉查找树一样。
### 6.3 插入
当在AVL中插入新的结点时,需要根据实际情况对AVL中的某些结点做单旋转或双旋转操作,单旋转表示做一次顺时针或逆时针的旋转操作,而双旋转则做两次单旋转操作(先顺时针后逆时针,或者先逆时针后顺时针),单旋转发生在LL型插入和RR型插入,而双旋转则发生在LR型插入和RL型插入。以下的失去平衡点都指的是离插入点最近的那个失去平衡的结点。
LL型:插入点位于失去平衡点的左孩子的左子树上;
RR型:插入点位于失去平衡点的右孩子的右子树上;
LR型:插入点位于失去平衡点的左孩子的右子树上;
RR型:插入点位于失去平衡点的右孩子的左子树上。
**插入思路**
和二叉搜索树的插入一样,首先在树中找到对应的位置然后插入,接着自底向上向根节点折回,于在插入期间成为不平衡的所有节点上进行旋转来完成。因为折回到根节点的路途上最多有 1.5 乘 log n 个节点,而每次AVL 旋转都耗费恒定的时间,插入处理在整体上耗费 O(log n) 时间。 
具体插入过程如下:
1. 如果当前结点为空,创建新结点返回.
2. 如果当前结点值和插入值相同,不做处理返回。
3. 如果插入值大于当前结点则插入到右其右孩子结点中。插入完成后比较左右孩子结点进行判断树是否失去平衡。如果是判断属于那种类型(RR, RL),并响应的旋转。最后更新当前结点的深度(孩子结点的最大深度加1,默认null深度为-1)。
4. 否则插入到其做孩子上。 同样比较左右孩子的深度判断是否平衡,对于失去平衡的情况下做出调整。
private AvlNode<AnyType> insert(AnyType x, AvlNode<AnyType> t) {
if (t == null)
return new AvlNode<AnyType>(x, null, null);
int compareResult = myCompare(x, t.element);
if (compareResult < 0) {
t.left = insert(x, t.left);
if (height(t.left) - height(t.right) == 2) {
if (myCompare(x, t.left.element) < 0) //左左情况
t = rotateWithLeftChild(t);
else //左右情况
t = doubleWithLeftChild(t);
}
} else if (compareResult > 0) {
t.right = insert(x, t.right);
if (height(t.right) - height(t.left) == 2) {
if (myCompare(x, t.right.element) < 0) //右左情况
t = doubleWithRightChild(t);
else //右右情况
t = rotateWithRightChild(t);
}
}
//完了之后更新height值
t.height = Math.max(height(t.left), height(t.right)) + 1;
return t;
}
### 6.4 删除
从AVL树中删除可以通过把要删除的节点向下旋转成一个叶子节点,接着直接剪除这个叶子节点来完成。因为在旋转成叶子节点期间最多有 log n个节点被旋转,而每次 AVL 旋转耗费恒定的时间,删除处理在整体上耗费 O(log n) 时间。
a.当被删除节点n是叶子节点,直接删除
b.当被删除节点n只有一个孩子,删除n,用孩子替代该节点的位置
c.当被删除结点n存在左右孩子时,真正的删除点应该是n的中序遍在前驱,或者说是左子树最大的节点,之后n的值替换为真正删除点的值。这就把c归结为a,b的问题。
从删除的结点处自低向上向根结点折回,根据当前结点的左右孩子深度判断是否平衡,如果不平衡则按选择规则进行旋转。最后更新当前结点深度,如此递归折回到根结点。
http://blog.csdn.net/xiaofan086/article/details/8294382
http://blog.csdn.net/liyong199012/article/details/29219261
http://www.mamicode.com/info-detail-90162.html
## 七. B-树(B树)
#### 7.1 定义
B-树是一种平衡的多路查找树,它在文件系统中很有用。
定义:一棵m 阶的B-树,或者为空树,或为满足下列特性的m 叉树:
⑴树中每个结点至多有m 棵子树;
⑵若根结点不是叶子结点,则至少有两棵子树;
⑶除根结点之外的所有非终端结点至少有⎡m/2⎤ 棵子树;
⑷所有的非终端结点中包含以下信息数据:(n,A0,K1,A1,K2,…,Kn,An)
其中:Ki(i=1,2,…,n)为关键码,且Ki< Ki+1,Ai 为指向子树根结点的指针(i=0,1,…,n),且指针Ai-1 所指子树中所有结点的关键码均小于Ki (i=1,2,…,n),An 所指子树中所有结点的关键码均大于Kn, ⎡m/2⎤ −1 ≤ n ≤m −1 ,n 为关键码的个数。
⑸所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空
#### 7.2 查找
B-树的查找是由两个基本操作交叉进行的过程,即
⑴在B-树上找结点; ⑵在结点中找关键码。
由于,通常B-树是存储在外存上的,操作⑴就是通过指针在磁盘相对定位,将结点信息读入内存,之后,再对结点中的关键码有序表进行顺序查找或折半查找。因为,在磁盘上读取结点信息比在内存中进行关键码查找耗时多,每次向下搜索一层都需要从内存中加载磁盘信息,B-树的层次树是决定B-树查找效率的首要因素。
#### 7.2 插入
1.插入
在B-树上插入关键码与在二叉排序树上插入结点不同,关键码的插入不是在叶结点上
进行的,而是在最底层的某个非终端结点中添加一个关键码,若该结点上关键码个数不超过m-1 个,则可直接插入到该结点上;否则,该结点上关键码个数至少达到m 个,因而使该结点的子树超过了m棵,这与B-树定义不符。所以要进行调整,即结点的“分裂”。方法为:关键码加入结点后,将结点中的关键码分成三部分,使得前后两部分关键码个数个结点将其插入到父结点中。若插入父结点而使父结点中关键码个数超过m-1,则父结点继续分裂,直到插入某个父结点,其关键码个数小于m。可见,B-树是从底向上生长的。
#### 7.3 删除
分两种情况:
(1)删除最底层结点中关键码
a)若结点中关键码个数大于⎡m / 2⎤ -1,直接删去。
b)否则除余项与左兄弟(无左兄弟,则找左兄弟)项数之和大于等于2( -1) 就与它
们父结点中的有关项一起重新分配
(2)删除为非底层结点中关键码
若所删除关键码非底层结点中的Ki,则可以指针Ai 所指子树中的最小关键码X 替代
Ki,然后,再删除关键码X,直到这个X 在最底层结点上,即转为(1)的情形
#### 7.2 B-树的特性:
1.关键字集合分布在整颗树中;
2.任何一个关键字出现且只出现在一个结点中;
3.搜索有可能在非叶子结点结束;
4.其搜索性能等价于在关键字全集内做一次二分查找;
5.自动层次控制;
http://c.biancheng.net/cpp/html/1028.html
## 八. B+树
#### 8.1 定义
⑴有n 棵子树的结点中含有n 个关键码;
⑵所有的叶子结点中包含了全部关键码的信息,及指向含有这些关键码记录的指针,且
叶子结点本身依关键码的大小自小而大的顺序链接。
⑶所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键码。
#### 8.2 B+的特性
1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
2.不可能在非叶子结点命中;
3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
4.更适合文件索引系统;
#### 8.3 操作
1)查找操作
对B+树可以进行两种查找运算:
a.从最小关键字起顺序查找;
b.从根结点开始,进行随机查找。
在查找时,若非终端结点上的剧组机等于给定值,并不终止,而是继续向下直到叶子结点。因此,在B+树中,不管查找成功与否,每次查找都是走了一条从根到叶子结点的路径。其余同B-树的查找类似。
2).插入操作
B+树的插入与B树的插入过程类似。不同的是B+树在叶结点上进行,如果叶结点中的关键码个数超过m,就必须分裂成关键码数目大致相同的两个结点,并保证上层结点中有这两个结点的最大关键码。(算法见百度百科)
3)删除操作
B+树的删除也仅在叶子结点进行,当叶子结点中的最大关键字被删除时,其在非终端结点中的值可以作为一个“分界关键字”存在。若因删除而使结点中关键字的个数少于m/2 (m/2结果取上界,如5/2结果为3)时,其和兄弟结点的合并过程亦和B-树类似。
### 7.2 B+树和B-树最大的不同点:
1).B-树的关键字和记录是放在一起的,叶子节点可以看作外部节点,不包含任何信息;B+树的非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。
2).在B-树中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而B+树中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中还要再比较关键字。从这个角度看B-树的性能好像要比B+树好,而在实际应用中却是B+树的性能要好些。因为B+树的非叶子节点不存放实际的数据,这样每个节点可容纳的元素个数比B-树多,树高比B-树小,这样带来的好处是减少磁盘访问次数。尽管B+树找到一个记录所需的比较次数要比B-树多,但是一次磁盘访问的时间相当于成百上千次内存比较的时间,因此实际中B+树的性能可能还会好些,而且B+树的叶子节点使用指针连接在一起,方便顺序遍历(例如查看一个目录下的所有文件,一个表中的所有记录等),这也是很多数据库和文件系统使用B+树的缘故。
3)B+树支持range-query非常方便,而B树不支持。这是数据库选用B+树的最主要原因
http://www.cnblogs.com/biyeymyhjob/archive/2012/07/25/2608880.html
http://blog.csdn.net/v_JULY_v/article/details/6530142/
## 8. 红黑树
#### 思想
红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
#### 特性
性质1. 节点是红色或黑色。
性质2. 根是黑色。
性质3. 所有叶子都是黑色(叶子是NIL节点)。
性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有简单路径 都包含相同数目的黑色节点。
#### 插入
II、红黑树插入的几种情况:
情况1,z的叔叔y是红色的。
情况2:z的叔叔y是黑色的,且z是右孩子
情况3:z的叔叔y是黑色的,且z是左孩子
#### 删除
III、红黑树删除的几种情况。
情况1:x的兄弟w是红色的。
情况2:x的兄弟w是黑色的,且w的俩个孩子都是黑色的。
情况3:x的兄弟w是黑色的,且w的左孩子是红色,w的右孩子是黑色。
情况4:x的兄弟w是黑色的,且w的右孩子是红色的。
http://www.cnblogs.com/daoluanxiaozi/p/3340382.html
#### 红黑树和AVL树的比较:
红黑树:
(1)并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。
(2)此外,由于它的设计,任何不平衡都会在三次旋转之内解决。红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。
AVL树:
(1)它的左子树和右子树都是AVL树,左子树和右子树的高度差不能超过;
(2) 查找、插入和删除在平均和最坏情况下都是O(log n),增加和删除可能需要通过一次或多次树旋转来重新平衡这个树;
(3)一棵n个结点的AVL树的其高度保持在0(log2(n)),不会超过3/2log2(n+1)
一棵n个结点的AVL树的平均搜索长度保持在0(log2(n)).
一棵n个结点的AVL树删除一个结点做平衡化旋转所需要的时间为0(log2(n)).
## 哈夫曼树。
个人公众号(欢迎关注):<br>
![](/assets/weix_gongzhonghao.jpg)
+235
View File
@@ -0,0 +1,235 @@
##排序
> 键索引计数法:
> - 频率统计:使用int数组count[]计算每个建出现的频率
> - 将频率转化为索引:使用count来计算每个键在排序结果中的起始索引位置
> - 数据分类:将所有元素转移到一个辅助数组aux中以进行排序。
> - 回写:将元素排序后的结果复制回原数组。
### 低优先字符排序
**思路**:从右到左以每个位置的字符为键,用键索引计数法将字符串排序W遍(字符串长度)
**复杂度**: O(NW) W为字符串长度,N为元素选择空间
实现:
public static void sort(String[] a, int w) {
// 通过前w个字符将a[] 排序
int N = a.length;
int R = 256;
String[] aux = new String[N];
for (int d = w - 1; d >= 0; d--) {
int[] count = new int[R + 1];
// 计算出频率
for (int i = 0; i < N; i++) {
count[a[i].charAt(d) + 1]++;
}
// 将频率转换为索引
for (int r = 0; r < R; r++) {
count[r + 1] += count[r];
}
// 将元素分类
for (int i = 0; i < N; i++ ) {
aux[count[a[i].charAt(d)] ++] = a[i];
}
for (int i = 0; i < N; i ++ ) {
a[i] = aux[i];
}
}
}
### 高位优先字符串排序
**思路**:解决字符串长度不一致的情况,采用从左到右的次序进行排序。 在高位优先排序时需要注意的字符串末尾的情况,因为字符串长度不一,合理的做法是将所有字符都已被检查的字符串所在的子数组排在所有子数组的前面,这样不需要对这些检查完成的字符串再次递归排序,也避免了IndexOutOfBound。 高位优先排序在已排号序的位置,根据字符进行分组,将数组分为更小的数组进行排序,利用了分治算法提高了算法的性能。
**复杂度**:O(N) 到O(Nw)之间
实现:
**三向字符串快速排序**
**思路**:结合了快速排序和高位优先字符串排序算法,采用高位优先字符串排序算法的思路,根据字符串从左到右的字符对字符串进行排序。排序时使用快速排序的思想,使用首字符进行三向切分为首字母小余、等于,大于切分字符的字符串数组,然后递归地将得到的三个字数组排序:
**优势**:高位优先字符串排序算法会产生大量的子数组,而三向字符串快速排序切分总只有三个。因此该算法能够很好处理等值键、有较长公告前缀的键、取值范围娇小的键和小数组。 相对于高位优先字符串排序需要的空间更少。
复杂度:
**实现:**
public static void sort(String[] strArr) {
sort(strArr, 0, strArr.length - 1, 0);
}
/**
* 对字符串串数组strArr的 strArr[lo]到strArr[hi]的字符串根据d为的字符进行排序
* @param strArr
* @param lo
* @param hi
* @param d
*/
private static void sort(String[] strArr, int lo, int hi , int d) {
if (hi <= lo) {
return;
}
int lt = lo, gt = hi;
int val = strArr[lo].charAt(d);
int i = lo + 1;
while(i <= gt) {
int tmp = strArr[i].charAt(d);
if ( tmp < val) {
exch(strArr, lt++, i++);
} else if( tmp > val) {
exch(strArr, i, gt--);
} else {
i ++;
}
}
sort(strArr, lo, lt-1, d);
if (val >= 0 ) {
sort(strArr, lt, gt, d+1);
}
sort(strArr, gt+1, hi, d);
}
private static void exch(String[] a, int i ,int j) {
String tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
> 注意:和快速排序一样,最好在排序之前将数组打乱或将第一个元素和一个随机位置的元素交换以得到一个随机的切分元素,避免快速排序算法接近最坏情况
总结:
| 算法 | 稳定性 | 原地排序|时间复杂度|空间复杂度|优势领域|
| -- | -- | -- | - | -| -|
|字符串的插入排序| 是|是|N到N2之间|1|小数组或已经有序的数组|
|快速排序|否|是|N(log2N)|logN|通用排序算法,特别适合空间不足的情况|
|归并排序|是|否|N(log2N)|N|稳定的通用排序算法|
|三向快速排序|否|是|N到NlogN之间|logN|大量重复建|
|低位优先的字符串排序|是|否|NW|N|较长的定长字符串|
|高位优先的字符串排序|是|否|N到Nw之间|N+WR|随机字符串|
|三向字符串快速排序|否|是|N到Nw之间|W+logN|通用排序算法,特别适合用于含有较长公共前缀的字符串|
查找树
- 单词查找树(trie)
基本性质: 是由连接结点组成的数据结构,除了根结点,每个结点有且只有一条从父结点指向它的链接。每条链接都对应着一个字符,也可以用链接所对应的字符标记被指向的结点。值为空的结点在符号表中没有对应的键,它们的存在是为了简化单词查找树的查找操作。
查找过程:从根结点开始,首先经过的键是首字母所对应的链接。在下一个结点中沿着第二个字符所对应的链接继续前进。依此类推,直到键的最后一个字符所指向的结点或者遇到一条空链接。此时有三种情况:
- 键的尾字符所对应的结点中的值非空,这是一次命中查找,则返回尾字符所对应的结点中保存的值。
- 键的尾字符所对应的结点中的值为空,表示未命中。
- 查找结束于一条空链接,表示未命中。
插入过程:进行插入的时候和二叉查找树一样,需要先进行查找,会遇到以下两个情况:
- 到达键尾部之前就遇到空链接,则创建对应的结点并将键值保存到最后一个字符的结点中。
- 在遇到空链接之前就到达了键的尾字符,在这种情况下,和关联性数组一样,将该结点的值设置为键所对应的值。
删除过程:首先找到对应的结点并将它的值设置为空,如果该结点含有一个非空的链接指向某个子结点那么就不需要进行其他操作。如果它所有链接均为空,那么就需要从数据结构中删去这个结点,如果删去它使得它的父节点的所有链接也均为空,就需要继续删除它的父结点,依此类推。
性质:
- 单词查找树的链表结构和键的插入或删除顺序无关:对于给定任意一组键,其单词查找树都是唯一的。
- 在单词查找树中查找一个键或插入一个键时,访问数组的次数最多的键的长度加1.
- 未命中查找平均所需检查的结点的数量为 logRN
- 一棵单词查找树的链接总数在RN到RNw之间,其中w为建的平均长度。 ???
应用:
- 数字帐号
- URL
- 文本处理
- 基因组数据中的蛋白质
代码实现:
- 三向单词查找树
思想:为了避免R向单词查找树过度的浪费空间,三向单词查找树每个结点都含有一个字符、三条链接和一个值。这三条链接分别对应着当前字母小于、等于和大于结点字母的所有键。
查找:查找时比较键的首字母和根结点的字母,如果键的首字母较小,就选择左链接。如果较大,就选择右链接;如果相等,则选择中链接。然后递归的使用相同的算法,如果遇到一个空链接或则当前键结束时结点的值为空,那么查找未命中;如果键结束时结点的值非空则查找命中。
优势:节省大量空间。
性质:
- 每个结点只有三个链接,链接总数在3N到3Nw之间
- 平均查找时间复杂度为lnN 次
代码实现:
子字符串查找
- 暴力字符串查找算法
- KMP算法
思想:KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,只要继续把它向后移和移动匹配词就可以,这样就提高了效率。可以针对搜索词,算出一张部分匹配表。通过查表查到最后一个匹配字符对应的部分匹配值,并利用以下公式计算匹配词向后移动的位数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
实现:
/**
* 计算部分匹配表
*
* @param pattern
* @param next
*/
public void makeNext(char[] pattern, int next[]) {
int pIdx, maxSuffixLen; // pIdx:模版字符串下标;maxSuffixLen:最大前后缀长度
int m = pattern.length; // 模版字符串长度
next[0] = 0; //模版字符串的第一个字符的最大前后缀长度为0
for (pIdx = 1, maxSuffixLen = 0; pIdx < m; ++pIdx) //for循环,从第二个字符开始,依次计算每一个字符对应的next值
{
/**
* maxSuffixLen 大于0 表示前一个字符已经存在匹配
*/
while (maxSuffixLen > 0 && pattern[pIdx] != pattern[maxSuffixLen]) { //递归的求出P[0]···P[q]的最大的相同的前后缀长度k
maxSuffixLen = next[maxSuffixLen - 1]; //不理解没关系看下面的分析,这个while循环是整段代码的精髓所在,确实不好理解
}
if (pattern[pIdx] == pattern[maxSuffixLen]) //如果相等,那么最大相同前后缀长度加1
{
maxSuffixLen++;
}
next[pIdx] = maxSuffixLen;
}
}
public int kmp(String str, String pattern) {
int[] next = new int[str.length()];
int strIdx, pIdx;
makeNext(pattern.toCharArray(), next);
for (strIdx = 0, pIdx = 0; strIdx < str.length(); ++strIdx) {
while (pIdx > 0 && pattern.charAt(pIdx) != str.charAt(strIdx)) {
/**
* 移动匹配字符串位置
*/
pIdx = next[pIdx - 1];
}
if (pattern.charAt(pIdx) == str.charAt(strIdx)) {
pIdx++;
}
if (pIdx == pattern.length()) {
return strIdx - pattern.length() + 1;
}
}
return -1;
}
复杂度:时间复杂度最坏(3N) 空间复杂度 O(M)
##参考资料
http://www.cnblogs.com/c-cloud/p/3224788.html
字符串S的最长回文子串S1
参考资料
https://github.com/JohnZhengHub/blogs/blob/master/algorithm/algorithm-base/Manacher.md
个人公众号(欢迎关注):<br>
![](/assets/weix_gongzhonghao.jpg)
View File