线段树、差分数组、树状数组

This commit is contained in:
yinkanglong_lab
2021-03-26 19:25:35 +08:00
parent 66d7ed981d
commit f07b498833
25 changed files with 569 additions and 321 deletions

View File

@@ -4,7 +4,7 @@
- [ ] 学习、复习图算法,动手实现所有的图算法。
- [ ] 看完数据结构与算法的三本书!!!对相关的原理进行复习和总结。
- [ ] 学习机器学习的实现方案。毕设计划真正的开始执行。
- [x] 学习机器学习的实现方案。毕设计划真正的开始执行。
- [ ] 关于字符串分割。字符串格式化方法的总结。转换成流,作为流对象处理。转换为容器。作为容器对象处理,使用泛型算法。
## 收获

View File

@@ -1,4 +1,14 @@
# 今日计划
## 计划
* 执行之前的计划
- [ ] 整理会议记录发到群里。
## 收获
# PPT准备
## 目标
## 工程

View File

View 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)
$$
### 本质理解
![](image/2021-03-26-18-06-40.png)
* 怎么看每个原始数组的数字管理多少?只要顺着原始数组的数字往下画竖线,碰到的第一根横线所覆盖的范围就是它能管理的范围。
* 怎么看每个原始数组的数字被谁管理?顺着原始数组的数字往下画竖线,碰到的第一根横线之后的所有横先,都是管理该数字的树状数组值。
* 在构建的**树状数组中**$2^i$管理之前所有位置的**原始数组**。即前i项和。
* 通过lowbit()运算,构建的树状数组,本质上是一种**辅助数组**。
![](image/2021-03-26-18-42-22.png)
## 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()对应的值相加。
* 那么对于任意的xsum(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 逆序对

View File

@@ -0,0 +1,234 @@
# 线段树
## 1 线段树的概念
### 概述
* 线段树(Segment Tree)也是一棵树,只不过元素的值代表一个区间。常用区间的 统计 操作,比如一个区间的 **最大值(max),最小值(min),和(sum)** 等等
* 如一个长度为10的数组它对应的 求和 线段树,如下图所示(图中的数字表示索引)
![](image/2021-03-26-18-52-43.png)
### 定义
* 线段树是一个平衡二叉树,但不一定是完全二叉树。
* 根节点就是 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]
}
```
* 对下面一个数组
![](image/2021-03-26-19-16-09.png)
* 就会构建成如下一个线段树(图中括号里数字表示索引区间)
![](image/2021-03-26-19-16-22.png)
### 修改线段树
* 针对上面的数组,把索引为 1 的值改成 6 如下图所示
![](image/2021-03-26-19-17-04.png)
* 那么线段树需要修改的节点有(虚线标明)
![](image/2021-03-26-19-17-16.png)
### 线段树的查询
* 对于线段树的查询,主要有以下几种情况:
* 要查询的区间在刚好就是当前节点的区间
* 要查找的区间在当前节点的左子树区间
* 要查找的区间在当前节点的右子树区间
* 要查找的区间一部分在当前节点的左子树区间,一部分在右子树区间
## 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();
}
}
```

View File

@@ -0,0 +1,110 @@
# 差分数组Sparse Array
## 1 定义
* 定义原数组为a差分数组为d那么有
$$
d[i] = a[i] - a[i - 1]
$$
![](image/2021-03-26-17-36-48.png)
* 其实差分数组是一个**辅助数组**,从侧面来表示给定某一数组的变化,一般用来对数组进行区间修改的操作
## 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 应用
### 区间修改
* 当对一个区间进行增减某个值的时候,他的差分数组对应的区间左端点的值会同步变化,而他的右端点的后一个值则会相反地变化。他们的差分数组其实是不会变化的。
* 例如将区间【14】的数值全部加上3
![](image/2021-03-26-17-47-36.png)
* [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.可以通过查分数组来表示区间的增加。最后计算某一时刻具有的会议数量。

View File

@@ -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)$
## 参考资料

View File

@@ -1,307 +0,0 @@
# 线性表的查找
## 概念
### 什么是查找?
**查找**是根据给定的某个值,在表中确定一个关键字的值等于给定值的记录或数据元素。
### 查找算法的分类
若在查找的同时对表记录做修改操作(如插入和删除),则相应的表称之为**动态查找表**
否则,称之为**静态查找表**。
此外,如果查找的全过程都在内存中进行,称之为**内查找**
反之,如果查找过程中需要访问外存,称之为**外查找**。
### 查找算法性能比较的标准
**——平均查找长度ASLAverage Search Length**
由于查找算法的主要运算是关键字的比较过程,所以通常把查找过程中对关键字需要执行的**平均比较长度**(也称为**平均比较次数**)作为衡量一个查找算法效率优劣的比较标准。
![img](https://upload-images.jianshu.io/upload_images/3101171-a38f84148d091364.gif?imageMogr2/auto-orient/strip)
**选取查找算法的因素**
(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是分块有序的所以**索引表是一个递增有序表**。
下图就是一个分块查找表的存储结构示意图
![img](https://upload-images.jianshu.io/upload_images/3101171-b7ad44c68d0c3c75.png)
**基本思想**
分块查找算法有两个处理步骤:
**(1) 首先查找索引表**
因为分块查找表是“分块有序”的,所以我们可以通过索引表来锁定关键字所在的区间。
又因为索引表是递增有序的,所以查找索引可以使用顺序查找或二分查找。
**(2) 然后在已确定的块中进行顺序查找**
因为块中不一定是有序的,所以只能使用顺序查找。
**代码范例**
![img](http://upload-images.jianshu.io/upload_images/3101171-2737612c781e66e8.gif?imageMogr2/auto-orient/strip)
```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版

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -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

View 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;
}
};
```

View File

@@ -25,7 +25,8 @@ $$
$$
res=max(res,ji)
$$
![](image/2021-03-19-01-02-12.png)
![](image/2021-03-26-15-18-18.png)
### 算法分析

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -106,7 +106,7 @@ private:
};
```
* 一下是官方的方法,内置函数
```
```C++
class Solution {
public:
string minNumber(vector<int>& nums) {