线段树、差分数组、树状数组
@@ -4,7 +4,7 @@
|
||||
|
||||
- [ ] 学习、复习图算法,动手实现所有的图算法。
|
||||
- [ ] 看完数据结构与算法的三本书!!!对相关的原理进行复习和总结。
|
||||
- [ ] 学习机器学习的实现方案。毕设计划真正的开始执行。
|
||||
- [x] 学习机器学习的实现方案。毕设计划真正的开始执行。
|
||||
- [ ] 关于字符串分割。字符串格式化方法的总结。转换成流,作为流对象处理。转换为容器。作为容器对象处理,使用泛型算法。
|
||||
|
||||
## 收获
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
# 今日计划
|
||||
|
||||
## 计划
|
||||
|
||||
* 执行之前的计划
|
||||
- [ ] 整理会议记录发到群里。
|
||||
|
||||
## 收获
|
||||
|
||||
|
||||
# PPT准备
|
||||
## 目标
|
||||
|
||||
## 工程
|
||||
|
||||
0
数据结构/3.1 单调栈.md
Normal file
114
数据结构/6.10 树状数组.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 树状数组Binary Tree Array
|
||||
|
||||
|
||||
## 1 概念
|
||||
|
||||
|
||||
### lowbit运算
|
||||
$$
|
||||
lowbit(x) = x \& ((\sim x)+1)
|
||||
$$
|
||||
* 作用:二进制表示中,保留最后的`1`。如`x=10100100`,`lowbit(x)=00000100`
|
||||
|
||||
### 树状数组
|
||||
|
||||
* 根据lowbit的运算规则构建树状数组。
|
||||
* 其中数组A第i个位置的值**管理区间**[i-lowbit(i)+1,i]。即去掉二进制最后一位后到当前位置的的区间。
|
||||
|
||||
$$
|
||||
A[i] = \sum_{k=i-lowbit(i)+1}^i A[k]
|
||||
$$
|
||||
|
||||
* 其中数组A第i个位置的值的**被管节点**。即找到所有的之后的$2^i$的点,也包括非$2^i$的点。
|
||||
|
||||
$$
|
||||
i = i + lowbit(i) ,i<max(i)
|
||||
$$
|
||||
|
||||
|
||||
### 本质理解
|
||||

|
||||
|
||||
* 怎么看每个原始数组的数字管理多少?只要顺着原始数组的数字往下画竖线,碰到的第一根横线所覆盖的范围就是它能管理的范围。
|
||||
* 怎么看每个原始数组的数字被谁管理?顺着原始数组的数字往下画竖线,碰到的第一根横线之后的所有横先,都是管理该数字的树状数组值。
|
||||
|
||||
* 在构建的**树状数组中**$2^i$管理之前所有位置的**原始数组**。即前i项和。
|
||||
* 通过lowbit()运算,构建的树状数组,本质上是一种**辅助数组**。
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 2 性质
|
||||
|
||||
* 任何一个数字管理
|
||||
|
||||
$$
|
||||
(i-lowbit(i),i]
|
||||
$$
|
||||
* 任何一个数字被管理
|
||||
|
||||
$$
|
||||
[i,i+lowbit(i)],进行迭代i=i+lowbit(i)
|
||||
$$
|
||||
|
||||
## 3 应用
|
||||
|
||||
### 把位置x的元素都加k
|
||||
|
||||
* 我们每次执行操作A(把位置x的值+k),只需要把"能管理到x的所有位置"都+k就行
|
||||
1. A[i]+k
|
||||
2. i=i+lowbit(i)
|
||||
3. 重复以上步骤,直到i>max截止
|
||||
|
||||
### 求前缀区间的值
|
||||
|
||||
> 所有lowbit()对应的值相加。
|
||||
|
||||
* 那么对于任意的x,sum(1,x)怎么求呢?我们把最终得到的答案存在ans变量中,执行下面的操作:
|
||||
1. sum=0
|
||||
2. sum += A[i]
|
||||
3. i = i-lowbit(i)
|
||||
4. 重复2-3步骤,直到i<0截止
|
||||
|
||||
|
||||
### 求区间的值
|
||||
|
||||
* 询问区间[L,R]的和sum(L,R)。我们只需要求出sum(1,R)和sum(1,L-1),然后sum(1,R)-sum(1,L-1)就是sum(L,R).
|
||||
$$
|
||||
sum(L,R)=sum(1,R)-sum(1,L)
|
||||
$$
|
||||
|
||||
## 实例1 基本运算
|
||||
|
||||
```C++
|
||||
//求最小幂2^k:
|
||||
int Lowbit(int t)
|
||||
{
|
||||
return t & ( t ^ ( t - 1 ) );
|
||||
}
|
||||
|
||||
//求前n项和:
|
||||
int Sum(int end)
|
||||
{
|
||||
int sum = 0;
|
||||
while(end > 0)
|
||||
{
|
||||
sum += in[end];
|
||||
end -= Lowbit(end);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
//对某个元素进行加法操作
|
||||
void plus(int pos , int num)
|
||||
{
|
||||
while(pos <= n)
|
||||
{
|
||||
in[pos] += num;
|
||||
pos += Lowbit(pos);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实例2 逆序对
|
||||
234
数据结构/6.11 线段树.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# 线段树
|
||||
|
||||
## 1 线段树的概念
|
||||
|
||||
### 概述
|
||||
* 线段树(Segment Tree)也是一棵树,只不过元素的值代表一个区间。常用区间的 统计 操作,比如一个区间的 **最大值(max),最小值(min),和(sum)** 等等
|
||||
|
||||
* 如一个长度为10的数组,它对应的 求和 线段树,如下图所示(图中的数字表示索引):
|
||||
|
||||

|
||||
|
||||
### 定义
|
||||
* 线段树是一个平衡二叉树,但不一定是完全二叉树。
|
||||
* 根节点就是 0~lenght-1 的和
|
||||
* 根节点的左右子树平分根节点的区间
|
||||
* 然后依次类推,直到只有一个元素不能划分为止,该元素也就是二叉树的叶子节点。
|
||||
|
||||
### 复杂度
|
||||
* 时间复杂度为O(log n)
|
||||
* 空间复杂度为O(n)
|
||||
|
||||
## 2 线段树的基本操作
|
||||
### 构建线段树
|
||||
|
||||
* 根据上面我们对线段树的描述,构建一个线段树就比较简单了,根节点就是整个区间,根节点的左右子树平分根节点的区间,直至区间内只剩下一个元素不能平分为止。如下面递归的伪代码:
|
||||
```C++
|
||||
private void buildSegmentTree(int treeIndex, int treeLeft, int treeRight) {
|
||||
//如果区间内只剩下一个元素
|
||||
if (treeLeft == treeRight) {
|
||||
tree[treeIndex] = data[treeLeft];
|
||||
return;
|
||||
}
|
||||
//当前节点左子树索引
|
||||
int leftTreeIndex = getLeft(treeIndex);
|
||||
//当前节点右子树索引
|
||||
int rightTreeIndex = getRight(treeIndex);
|
||||
//int mid = (left+right)/2;
|
||||
int mid = treeLeft + (treeRight - treeLeft) / 2;
|
||||
//构建左子树
|
||||
buildSegmentTree(leftTreeIndex, treeLeft, mid);
|
||||
//构建右子树
|
||||
buildSegmentTree(rightTreeIndex, mid + 1, treeRight);
|
||||
//当前节点存放的值,根据具体业务,如果求和就是两个值相加
|
||||
//如果是求最大值,那么就存放最大值
|
||||
tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex]
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
* 对下面一个数组
|
||||

|
||||
|
||||
|
||||
* 就会构建成如下一个线段树(图中括号里数字表示索引区间)
|
||||
|
||||

|
||||
|
||||
|
||||
### 修改线段树
|
||||
|
||||
* 针对上面的数组,把索引为 1 的值改成 6 如下图所示
|
||||
|
||||

|
||||
|
||||
|
||||
* 那么线段树需要修改的节点有(虚线标明):
|
||||
|
||||

|
||||
|
||||
### 线段树的查询
|
||||
* 对于线段树的查询,主要有以下几种情况:
|
||||
* 要查询的区间在刚好就是当前节点的区间
|
||||
* 要查找的区间在当前节点的左子树区间
|
||||
* 要查找的区间在当前节点的右子树区间
|
||||
* 要查找的区间一部分在当前节点的左子树区间,一部分在右子树区间
|
||||
|
||||
## 3 实现一个线段树
|
||||
* 下面实现的线段树,有三个功能:
|
||||
* 把数组构建成一颗线段树
|
||||
* 线段树的修改
|
||||
* 线段树的查询
|
||||
```java
|
||||
public class ArraySegmentTree<T> {
|
||||
|
||||
private T tree[];
|
||||
private T data[];
|
||||
|
||||
private Merger<T> merger;
|
||||
|
||||
public interface Merger<T> {
|
||||
T merge(T a, T b);
|
||||
}
|
||||
|
||||
public ArraySegmentTree(T[] arr, Merger<T> merger) {
|
||||
this.merger = merger;
|
||||
data = (T[]) new Object[arr.length];
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] = arr[i];
|
||||
}
|
||||
|
||||
this.tree = (T[]) new Object[data.length * 4];
|
||||
buildSegmentTree(0, 0, data.length - 1);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建线段树
|
||||
*
|
||||
* @param treeIndex 当前需要添加节点的索引
|
||||
* @param treeLeft treeIndex左边界
|
||||
* @param treeRight treeIndex右边界
|
||||
*/
|
||||
private void buildSegmentTree(int treeIndex, int treeLeft, int treeRight) {
|
||||
if (treeLeft == treeRight) {
|
||||
tree[treeIndex] = data[treeLeft];
|
||||
return;
|
||||
}
|
||||
//当前节点左子树索引
|
||||
int leftTreeIndex = getLeft(treeIndex);
|
||||
//当前节点右子树索引
|
||||
int rightTreeIndex = getRight(treeIndex);
|
||||
//int mid = (left+right)/2; 如果left和right很大,可能会导致整型溢出
|
||||
int mid = treeLeft + (treeRight - treeLeft) / 2;
|
||||
//构建左子树
|
||||
buildSegmentTree(leftTreeIndex, treeLeft, mid);
|
||||
//构建右子树
|
||||
buildSegmentTree(rightTreeIndex, mid + 1, treeRight);
|
||||
//当前节点存放的值
|
||||
tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
|
||||
|
||||
}
|
||||
|
||||
public T query(int start, int end) {
|
||||
return query(0, 0, data.length - 1, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param treeIndex 当前查找的节点
|
||||
* @param treeLeft treeIndex的左边界
|
||||
* @param treeRight treeIndex的右边界
|
||||
* @param queryL 用户需要查找的左边界
|
||||
* @param queryR 用户需要查找的右边界
|
||||
* @return
|
||||
*/
|
||||
private T query(int treeIndex, int treeLeft, int treeRight, int queryL, int queryR) {
|
||||
|
||||
//1, 需要查找的范围完刚好在这个treeIndex节点的区间
|
||||
if (treeLeft == queryL && treeRight == queryR) {
|
||||
return tree[treeIndex];
|
||||
}
|
||||
|
||||
//当前节点的区间的中间点
|
||||
int mid = treeLeft + (treeRight - treeLeft) / 2;
|
||||
//左子树索引
|
||||
int leftTreeIndex = getLeft(treeIndex);
|
||||
//右子树索引
|
||||
int rightTreeIndex = getRight(treeIndex);
|
||||
|
||||
|
||||
//2, 需要查找的范围完全在左子树的区间里
|
||||
if (queryR <= mid) {
|
||||
return query(leftTreeIndex, treeLeft, mid, queryL, queryR);
|
||||
}
|
||||
//3, 需要查找的范围完全在右子树区间里
|
||||
if (queryL >= mid + 1) {
|
||||
return query(rightTreeIndex, mid + 1, treeRight, queryL, queryR);
|
||||
}
|
||||
|
||||
//需要查找的范围一部分在左子树里,一部分在右子树中
|
||||
T left = query(leftTreeIndex, treeLeft, mid, queryL, mid);
|
||||
T right = query(rightTreeIndex, mid + 1, treeRight, mid + 1, queryR);
|
||||
return merger.merge(left, right);
|
||||
}
|
||||
|
||||
|
||||
public void update(int index, T e) {
|
||||
data[index] = e;
|
||||
update(0, 0, data.length - 1, index, e);
|
||||
}
|
||||
|
||||
|
||||
private void update(int treeIndex, int treeLeft, int treeRight, int index, T e) {
|
||||
if (treeLeft == treeRight) {
|
||||
tree[treeIndex] = e;
|
||||
return;
|
||||
}
|
||||
|
||||
int mid = treeLeft + (treeRight - treeLeft) / 2;
|
||||
int leftChildIndex = getLeft(treeIndex);
|
||||
int rightChildIndex = getRight(treeIndex);
|
||||
|
||||
if (index <= mid) {
|
||||
update(leftChildIndex, treeLeft, mid, index, e);
|
||||
} else if (index >= mid + 1) {
|
||||
update(rightChildIndex, mid + 1, treeRight, index, e);
|
||||
}
|
||||
|
||||
//更改完叶子节点后,还需要对他的所有祖辈节点更新
|
||||
tree[treeIndex] = merger.merge(tree[leftChildIndex], tree[rightChildIndex]);
|
||||
}
|
||||
|
||||
public T get(int index) {
|
||||
return data[0];
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return data.length;
|
||||
}
|
||||
|
||||
public int getLeft(int index) {
|
||||
return index * 2 + 1;
|
||||
}
|
||||
|
||||
public int getRight(int index) {
|
||||
return index * 2 + 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[");
|
||||
for (int i = 0; i < tree.length; i++) {
|
||||
if (tree[i] == null) {
|
||||
continue;
|
||||
}
|
||||
builder.append(tree[i]).append(',');
|
||||
}
|
||||
builder.deleteCharAt(builder.length() - 1);
|
||||
builder.append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
```
|
||||
110
数据结构/6.9 差分数组.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# 差分数组Sparse Array
|
||||
|
||||
|
||||
## 1 定义
|
||||
|
||||
* 定义:原数组为a,差分数组为d,那么有
|
||||
$$
|
||||
d[i] = a[i] - a[i - 1]
|
||||
$$
|
||||
|
||||

|
||||
|
||||
* 其实差分数组是一个**辅助数组**,从侧面来表示给定某一数组的变化,一般用来对数组进行区间修改的操作
|
||||
|
||||
## 2 性质
|
||||
1. a[i]等于d[i]的前缀和
|
||||
|
||||
$$
|
||||
a[i] = \sum_{0}^i d_i
|
||||
$$
|
||||
|
||||
2. d[i]等于a[i]两个临近元素的差
|
||||
$$
|
||||
d[i] = a[i] - a[i - 1]
|
||||
$$
|
||||
|
||||
## 3 应用
|
||||
### 区间修改
|
||||
|
||||
* 当对一个区间进行增减某个值的时候,他的差分数组对应的区间左端点的值会同步变化,而他的右端点的后一个值则会相反地变化。他们的差分数组其实是不会变化的。
|
||||
|
||||
* 例如:将区间【1,4】的数值全部加上3
|
||||
|
||||

|
||||
|
||||
* [l,r]区间内的数加k可以表示为如下形式:
|
||||
|
||||
$$
|
||||
d[l]+k\\
|
||||
d[r+1]-k
|
||||
$$
|
||||
|
||||
### 元素求值
|
||||
* 既然我们要对区间进行修改,那么差分数组的作用一定就是求多次进行区间修改后的数组。
|
||||
* 直接反过来即得
|
||||
$$
|
||||
a[i]=a[i-1]+d[i]
|
||||
$$
|
||||
|
||||
## 实例1 区间涂色
|
||||
|
||||
|
||||
### 问题描述
|
||||
|
||||
* N个气球排成一排,从左到右依次编号为1,2,3....N.每次给定2个整数a b(a <= b),lele便为骑上他的“小飞鸽"牌电动车从气球a开始到气球b依次给每个气球涂一次颜色。但是N次以后lele已经忘记了第I个气球已经涂过几次颜色了,你能帮他算出每个气球被涂过几次颜色吗?
|
||||
|
||||
### 问题分析
|
||||
|
||||
* 多次区间修改后求数组的值。
|
||||
|
||||
|
||||
### 算法实现
|
||||
|
||||
```C++
|
||||
#include<iostream>
|
||||
#include<cstdio>
|
||||
#include<algorithm>
|
||||
#include<cstring>
|
||||
#include<string>
|
||||
#include<cmath>
|
||||
#define ll long long
|
||||
#define mem(a,b) memset(a,b,sizeof(a))
|
||||
using namespace std;
|
||||
const int inf=0x3f3f3f3f;
|
||||
const int mm=1e5+10;
|
||||
|
||||
int a[mm],b[mm];
|
||||
int x,y;
|
||||
int main()
|
||||
{
|
||||
int n;
|
||||
while(scanf("%d",&n)&&n){
|
||||
mem(a,0);
|
||||
mem(b,0);
|
||||
for(int i=1;i<=n;i++){
|
||||
scanf("%d%d",&x,&y);
|
||||
b[x]++;
|
||||
b[y+1]--;
|
||||
}
|
||||
for(int i=1;i<=n;i++)
|
||||
a[i]=a[i-1]+b[i];
|
||||
|
||||
for(int i=1;i<n;i++)
|
||||
printf("%d ",a[i]);
|
||||
printf("%d\n",a[n]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 实例2 最多同时进行的会议
|
||||
|
||||
### 问题描述
|
||||
|
||||
给出会议的起始时间和截止时间。计算同一时间最多有多少场会议。
|
||||
|
||||
### 问题分析
|
||||
|
||||
会议持续一个时间段。该时间段都加1.可以通过查分数组来表示区间的增加。最后计算某一时刻具有的会议数量。
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
|
||||
在一个单链表中查询某个数据的时间复杂度是 O(n)。
|
||||
|
||||
在一个具有多级索引的跳表中,第一级索引的结点个数大约就是 n/2,第二级索引的结点个数大约就是 n/4,第三级索引的结点个数大约就是 n/8,依次类推,也就是说,第 k 级索引的结点个数是第 k-1 级索引的结点个数的 1/2,那第 k 级索引结点的个数就是 $$n/(2^k)$$
|
||||
在一个具有多级索引的跳表中,第一级索引的结点个数大约就是 n/2,第二级索引的结点个数大约就是 n/4,第三级索引的结点个数大约就是 n/8,依次类推,也就是说,第 k 级索引的结点个数是第 k-1 级索引的结点个数的 1/2,那第 k 级索引结点的个数就是 $n/(2^k)$
|
||||
|
||||
## 参考资料
|
||||
307
数据结构/9 跳表查找.md
@@ -1,307 +0,0 @@
|
||||
# 线性表的查找
|
||||
|
||||
## 概念
|
||||
|
||||
### 什么是查找?
|
||||
|
||||
**查找**是根据给定的某个值,在表中确定一个关键字的值等于给定值的记录或数据元素。
|
||||
|
||||
### 查找算法的分类
|
||||
|
||||
若在查找的同时对表记录做修改操作(如插入和删除),则相应的表称之为**动态查找表**;
|
||||
|
||||
否则,称之为**静态查找表**。
|
||||
|
||||
此外,如果查找的全过程都在内存中进行,称之为**内查找**;
|
||||
|
||||
反之,如果查找过程中需要访问外存,称之为**外查找**。
|
||||
|
||||
### 查找算法性能比较的标准
|
||||
|
||||
**——平均查找长度ASL(Average Search Length)**
|
||||
|
||||
由于查找算法的主要运算是关键字的比较过程,所以通常把查找过程中对关键字需要执行的**平均比较长度**(也称为**平均比较次数**)作为衡量一个查找算法效率优劣的比较标准。
|
||||
|
||||

|
||||
|
||||
**选取查找算法的因素**
|
||||
|
||||
(1) 使用什么数据存储结构(如线性表、树形表等)。
|
||||
|
||||
(2) 表中的次序,即对无序表还是有序表进行查找。
|
||||
|
||||
## 顺序查找
|
||||
|
||||
**要点**
|
||||
|
||||
它是一种最简单的查找算法,效率也很低下。
|
||||
|
||||
**存储结构**
|
||||
|
||||
没有存储结构要求,可以无序,也可以有序。
|
||||
|
||||
**基本思想**
|
||||
|
||||
从数据结构线形表的**一端**开始,**顺序扫描**,**依次**将扫描到的结点关键字与给定值k相**比较**,若相等则表示查找成功;
|
||||
|
||||
若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
|
||||
|
||||
**核心代码**
|
||||
|
||||
```java
|
||||
public int orderSearch(int[] list, int length, int key) {
|
||||
// 从前往后扫描list数组,如果有元素的值与key相等,直接返回其位置
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (key == list[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果扫描完,说明没有元素的值匹配key,返回-1,表示查找失败
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
**算法分析**
|
||||
|
||||
顺序查找算法**最好的情况**是,第一个记录即匹配关键字,则需要比较 **1** 次;
|
||||
|
||||
**最坏的情况**是,最后一个记录匹配关键字,则需要比较 **N** 次。
|
||||
|
||||
所以,顺序查找算法的平均查找长度为
|
||||
|
||||
ASL = (N + N-1 + ... + 2 + 1) / N = (N+1) / 2
|
||||
|
||||
顺序查找的**平均时间复杂度**为**O(N)**。
|
||||
|
||||
## 二分查找
|
||||
|
||||
> **二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0**。
|
||||
|
||||
**存储结构**
|
||||
|
||||
使用二分查找需要两个前提:
|
||||
|
||||
(1) 必须是**顺序**存储结构。
|
||||
|
||||
(2) 必须是**有序**的表。
|
||||
|
||||
**基本思想**
|
||||
|
||||
首先,将表**中间位置**记录的关键字与查找关键字比较,如果两者相等,则查找成功;
|
||||
|
||||
否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。
|
||||
重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
|
||||
|
||||
**核心代码**
|
||||
|
||||
```java
|
||||
public int binarySearch(int[] list, int length, int key) {
|
||||
int low = 0, mid = 0, high = length - 1;
|
||||
while (low <= high) {
|
||||
mid = (low + high) / 2;
|
||||
if (list[mid] == key) {
|
||||
return mid; // 查找成功,直接返回位置
|
||||
} else if (list[mid] < key) {
|
||||
low = mid + 1; // 关键字大于中间位置的值,则在大值区间[mid+1, high]继续查找
|
||||
} else {
|
||||
high = mid - 1; // 关键字小于中间位置的值,则在小值区间[low, mid-1]继续查找
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
**算法分析**
|
||||
|
||||
**二分查找的过程可看成一个二叉树**。
|
||||
|
||||
把查找区间的中间位置视为树的根,左区间和右区间视为根的左子树和右子树。
|
||||
|
||||
由此得到的二叉树,称为二分查找的判定树或比较树。
|
||||
|
||||
由此可知,二分查找的**平均查找长度**实际上就是树的高度**O(log<sub>2</sub>N)**。
|
||||
|
||||
**二分查找的局限性**
|
||||
|
||||
- 二分查找依赖的是顺序表结构,简单点说就是数组
|
||||
- 二分查找针对的是有序数据
|
||||
- 数据量太小不适合二分查找
|
||||
- 数据量太大也不适合二分查找
|
||||
|
||||
## 分块查找
|
||||
|
||||
**要点**
|
||||
|
||||
分块查找(Blocking Search)又称**索引顺序查找**。它是一种性能介于顺序查找和二分查找之间的查找方法。
|
||||
|
||||
分块查找由于只要求索引表是有序的,对块内节点没有排序要求,因此特别适合于节点动态变化的情况。
|
||||
|
||||
**存储结构**
|
||||
|
||||
分块查找表是由**“分块有序”的线性表**和**索引表**两部分构成的。
|
||||
|
||||
所谓**“分块有序”的线性表**,是指:
|
||||
|
||||
假设要排序的表为R[0...N-1],**将表均匀分成b块**,前b-1块中记录个数为s=N/b,最后一块记录数小于等于s;
|
||||
|
||||
每一块中的关键字不一定有序,但**前一块中的最大关键字必须小于后一块中的最小关键字**。
|
||||
|
||||
***注:这是使用分块查找的前提条件。***
|
||||
|
||||
如上将表均匀分成b块后,抽取各块中的**最大关键字**和**起始位置**构成一个索引表IDX[0...b-1]。
|
||||
|
||||
由于表R是分块有序的,所以**索引表是一个递增有序表**。
|
||||
|
||||
下图就是一个分块查找表的存储结构示意图
|
||||
|
||||

|
||||
|
||||
**基本思想**
|
||||
|
||||
分块查找算法有两个处理步骤:
|
||||
|
||||
**(1) 首先查找索引表**
|
||||
|
||||
因为分块查找表是“分块有序”的,所以我们可以通过索引表来锁定关键字所在的区间。
|
||||
|
||||
又因为索引表是递增有序的,所以查找索引可以使用顺序查找或二分查找。
|
||||
|
||||
**(2) 然后在已确定的块中进行顺序查找**
|
||||
|
||||
因为块中不一定是有序的,所以只能使用顺序查找。
|
||||
|
||||
**代码范例**
|
||||
|
||||

|
||||
|
||||
```java
|
||||
class BlockSearch {
|
||||
|
||||
class IndexType {
|
||||
public int key; // 分块中的最大值
|
||||
public int link; // 分块的起始位置
|
||||
}
|
||||
|
||||
// 建立索引方法,n 是线性表最大长度,gap是分块的最大长度
|
||||
public IndexType[] createIndex(int[] list, int n, int gap) {
|
||||
int i = 0, j = 0, max = 0;
|
||||
int num = n / gap;
|
||||
IndexType[] idxGroup = new IndexType[num]; // 根据步长数分配索引数组大小
|
||||
|
||||
while (i < num) {
|
||||
j = 0;
|
||||
idxGroup[i] = new IndexType();
|
||||
idxGroup[i].link = gap * i; // 确定当前索引组的第一个元素位置
|
||||
max = list[gap * i]; // 每次假设当前组的第一个数为最大值
|
||||
// 遍历这个分块,找到最大值
|
||||
while (j < gap) {
|
||||
if (max < list[gap * i + j]) {
|
||||
max = list[gap * i + j];
|
||||
}
|
||||
j++;
|
||||
}
|
||||
idxGroup[i].key = max;
|
||||
i++;
|
||||
}
|
||||
|
||||
return idxGroup;
|
||||
}
|
||||
|
||||
// 分块查找算法
|
||||
public int blockSearch(IndexType[] idxGroup, int m, int[] list, int n, int key) {
|
||||
int mid = 0;
|
||||
int low = 0;
|
||||
int high = m -1;
|
||||
int gap = n / m; // 分块大小等于线性表长度除以组数
|
||||
|
||||
// 先在索引表中进行二分查找,找到的位置存放在 low 中
|
||||
while (low <= high) {
|
||||
mid = (low + high) / 2;
|
||||
if (idxGroup[mid].key >= key) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 在索引表中查找成功后,再在线性表的指定块中进行顺序查找
|
||||
if (low < m) {
|
||||
for (int i = idxGroup[low].link; i < idxGroup[low].link + gap; i++) {
|
||||
if (list[i] == key)
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 打印完整序列
|
||||
public void printAll(int[] list) {
|
||||
for (int value : list) {
|
||||
System.out.print(value + " ");
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
// 打印索引表
|
||||
public void printIDX(IndexType[] list) {
|
||||
System.out.println("构造索引表如下:");
|
||||
for (IndexType elem : list) {
|
||||
System.out.format("key = %d, link = %d\n", elem.key, elem.link);
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int key = 85;
|
||||
int array2[] = { 8, 14, 6, 9, 10, 22, 34, 18, 19, 31, 40, 38, 54, 66, 46, 71, 78, 68, 80, 85 };
|
||||
BlockSearch search = new BlockSearch();
|
||||
|
||||
System.out.print("线性表: ");
|
||||
search.printAll(array2);
|
||||
|
||||
IndexType[] idxGroup = search.createIndex(array2, array2.length, 5);
|
||||
search.printIDX(idxGroup);
|
||||
int pos = search.blockSearch(idxGroup, idxGroup.length, array2,
|
||||
array2.length, key);
|
||||
if (-1 == pos) {
|
||||
System.out.format("查找key = %d失败", key);
|
||||
} else {
|
||||
System.out.format("查找key = %d成功,位置为%d", key, pos);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
**运行结果**
|
||||
|
||||
```
|
||||
线性表: 8 14 6 9 10 22 34 18 19 31 40 38 54 66 46 71 78 68 80 85
|
||||
构造索引表如下:
|
||||
key = 14, link = 0
|
||||
key = 34, link = 5
|
||||
key = 66, link = 10
|
||||
key = 85, link = 15
|
||||
|
||||
查找key = 85成功,位置为19
|
||||
```
|
||||
|
||||
**算法分析**
|
||||
|
||||
因为分块查找实际上是两次查找过程之和。若以二分查找来确定块,显然它的查找效率介于顺序查找和二分查找之间。
|
||||
|
||||
## 三种线性查找的PK
|
||||
|
||||
(1) 以平均查找长度而言,二分查找 > 分块查找 > 顺序查找。
|
||||
|
||||
(2) 从适用性而言,顺序查找无限制条件,二分查找仅适用于有序表,分块查找要求“分块有序”。
|
||||
|
||||
(3) 从存储结构而言,顺序查找和分块查找既可用于顺序表也可用于链表;而二分查找只适用于顺序表。
|
||||
|
||||
(4) 分块查找综合了顺序查找和二分查找的优点,既可以较为快速,也能使用动态变化的要求。
|
||||
|
||||
## 资源
|
||||
|
||||
《数据结构习题与解析》(B级第3版)
|
||||
BIN
数据结构/image/2021-03-26-17-36-48.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
数据结构/image/2021-03-26-17-47-36.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
数据结构/image/2021-03-26-18-06-40.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
数据结构/image/2021-03-26-18-42-22.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
数据结构/image/2021-03-26-18-52-43.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
数据结构/image/2021-03-26-19-16-09.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
数据结构/image/2021-03-26-19-16-22.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
数据结构/image/2021-03-26-19-17-04.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
数据结构/image/2021-03-26-19-17-16.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
@@ -4,20 +4,20 @@
|
||||
|
||||
### 位运算
|
||||
|
||||
符号|说明
|
||||
|----|----|
|
||||
& | 按位与
|
||||
\| | 按位或
|
||||
^ | 按位异或
|
||||
\~ | 按位取反
|
||||
\>\> | 右移
|
||||
<< | 左移
|
||||
符号|说明|特性
|
||||
|----|----|---|
|
||||
& | 按位与 | 只要有0,返回0
|
||||
\| | 按位或| 只要有1,返回1
|
||||
^ | 按位异或| 相同返回0。不同返回1
|
||||
\~ | 按位取反| 全部反转
|
||||
\>\> | 右移|
|
||||
<< | 左移|
|
||||
|
||||
## 2 特殊性质
|
||||
操作 | 性质
|
||||
|-----| -----|
|
||||
n & (n - 1) | n中的最后一个1变成0
|
||||
^ | 相同的数抑或运算等于零。不同的数抑或运算等于1
|
||||
n & (n - 1) | n中的最后一个1变成0。
|
||||
n & (\~n + 1) | lowbit()运算,n中最后一个1保留。
|
||||
n/2 | 等价于 右移一位 n >> 1
|
||||
n*2 | 等价于 左移一位 n << 1
|
||||
n % 2 |等价于 判断二进制最右一位值 n \& 1
|
||||
|
||||
86
算法/A类:基本算法/4.7 数组中的逆序对.md
Normal file
@@ -0,0 +1,86 @@
|
||||
## 1 数组中的逆序对
|
||||
|
||||
### 问题描述
|
||||
|
||||
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
|
||||
|
||||
### 问题分析
|
||||
|
||||
|
||||
|
||||
### 策略选择
|
||||
|
||||
* 数据结构:线性数组
|
||||
* 算法思想:变质法。将搜索查找问题修改为排序问题。归并排序
|
||||
|
||||
### 算法设计
|
||||
|
||||
* 归并排序」是分治思想的典型应用,它包含这样三个步骤:
|
||||
* 分解: 待排序的区间为 [l, r][l,r],令 m = \lfloor \frac{l + r}{2} \rfloorm=⌊2l+r⌋,我们把 [l, r][l,r] 分成 [l, m][l,m] 和 [m + 1, r][m+1,r]
|
||||
* 解决: 使用归并排序递归地排序两个子序列
|
||||
* 合并: 把两个已经排好序的子序列 [l, m][l,m] 和 [m + 1, r][m+1,r] 合并起来
|
||||
|
||||
* 在归并排序过程中。后边列表的数添加到前边之后。前边列表中数的量,就是本次排序逆序数的量。
|
||||
|
||||
### 算法分析
|
||||
|
||||
* 时间复杂度:同归并排序 O(n \log n)O(nlogn)。
|
||||
* 空间复杂度:同归并排序 O(n)O(n),
|
||||
|
||||
### 算法实现
|
||||
|
||||
```C++
|
||||
#include<iostream>
|
||||
#include<vector>
|
||||
#include<algorithm>
|
||||
|
||||
using namespace std;
|
||||
class Solution {
|
||||
public:
|
||||
int reversePairs(vector<int>& nums) {
|
||||
|
||||
int count=0;
|
||||
vector<int> temps(nums.size());
|
||||
merge_sort(nums,0,nums.size()-1,count,temps);
|
||||
return count;
|
||||
|
||||
}
|
||||
int merge_sort(vector<int>&nums,int beg,int end,int &count,vector<int>&temps){
|
||||
//进行头递归,分解
|
||||
int mid=0;
|
||||
if(beg<end){
|
||||
mid=(beg+end)/2;
|
||||
merge_sort(nums,beg,mid,count,temps);
|
||||
merge_sort(nums,mid+1,end,count,temps);
|
||||
}
|
||||
else{
|
||||
return 0;
|
||||
}
|
||||
|
||||
//进行合并
|
||||
int k=beg,i=beg,j=mid+1;
|
||||
while(k<=end){
|
||||
if(i==mid+1){
|
||||
temps[k++]=nums[j++];
|
||||
continue;
|
||||
}
|
||||
if(j==end+1){
|
||||
temps[k++]=nums[i++];
|
||||
continue;
|
||||
}
|
||||
if(nums[i]<=nums[j]){
|
||||
temps[k++]=nums[i++];
|
||||
}
|
||||
else{
|
||||
temps[k++]=nums[j++];
|
||||
count+=mid-i+1;
|
||||
}
|
||||
}
|
||||
for(int m=beg;m<=end;m++){
|
||||
nums[m]=temps[m];
|
||||
}
|
||||
return 0;
|
||||
|
||||
}
|
||||
};
|
||||
```
|
||||
@@ -25,7 +25,8 @@ $$
|
||||
$$
|
||||
res=max(res,j−i)
|
||||
$$
|
||||

|
||||
|
||||

|
||||
|
||||
### 算法分析
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB |
BIN
算法/B类:数据结构算法/image/2021-03-26-15-18-18.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
@@ -106,7 +106,7 @@ private:
|
||||
};
|
||||
```
|
||||
* 一下是官方的方法,内置函数
|
||||
```
|
||||
```C++
|
||||
class Solution {
|
||||
public:
|
||||
string minNumber(vector<int>& nums) {
|
||||
|
||||