Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
yanglbme authored Jun 27, 2020
2 parents 4b5e79b + 18d5d4f commit 84b0542
Show file tree
Hide file tree
Showing 247 changed files with 3,666 additions and 1,007 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Sync

on:
push:
branches: [ master ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Sync to Gitee
uses: wearerequired/git-mirror-action@master
env:
SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}
with:
source-repo: "[email protected]:doocs/advanced-java.git"
destination-repo: "[email protected]:Doocs/advanced-java.git"

- name: Build Gitee Pages
uses: yanglbme/gitee-pages-action@master
with:
gitee-username: yanglbme
gitee-password: ${{ secrets.GITEE_PASSWORD }}
gitee-repo: doocs/advanced-java
250 changes: 171 additions & 79 deletions README.md

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions docs/big-data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 海量数据处理

* [如何从大量的 URL 中找出相同的 URL?](/docs/big-data/find-common-urls.md)
* [如何从大量数据中找出高频词?](/docs/big-data/find-top-100-words.md)
* [如何找出某一天访问百度网站最多的 IP?](/docs/big-data/find-top-1-ip.md)
* [如何在大量的数据中找出不重复的整数?](/docs/big-data/find-no-repeat-number.md)
* [如何在大量的数据中判断一个数是否存在?](/docs/big-data/find-a-number-if-exists.md)
* [如何查询最热门的查询串?](/docs/big-data/find-hotest-query-string.md)
* [如何统计不同电话号码的个数?](/docs/big-data/count-different-phone-numbers.md)
* [如何从 5 亿个数中找出中位数?](/docs/big-data/find-mid-value-in-500-millions.md)
* [如何按照 query 的频度排序?](/docs/big-data/sort-the-query-strings-by-counts.md)
* [如何找出排名前 500 的数?](/docs/big-data/find-rank-top-500-numbers.md)

---

## 公众号

GitHub 技术社区 [Doocs](https://github.com/doocs) 旗下唯一公众号「**Doocs开源社区**」​,欢迎扫码关注,**专注分享技术领域相关知识及行业最新资讯**。当然,也可以加我个人微信(备注:GitHub),拉你进技术交流群。

<table>
<tr>
<td align="center" style="width: 200px;">
<a href="https://github.com/doocs">
<img src="./images/qrcode-for-doocs.jpg" style="width: 400px;"><br>
<sub>公众平台</sub>
</a><br>
</td>
<td align="center" style="width: 200px;">
<a href="https://github.com/yanglbme">
<img src="./images/qrcode-for-yanglbme.jpg" style="width: 400px;"><br>
<sub>个人微信</sub>
</a><br>
</td>
</tr>
</table>
19 changes: 19 additions & 0 deletions docs/big-data/count-different-phone-numbers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## 如何统计不同电话号码的个数?

### 题目描述

已知某个文件内包含一些电话号码,每个号码为 8 位数字,统计不同号码的个数。

### 解答思路

这道题本质还是求解**数据重复**的问题,对于这类问题,一般首先考虑位图法。

对于本题,8 位电话号码可以表示的号码个数为 10<sup>8</sup> 个,即 1 亿个。我们每个号码用一个 bit 来表示,则总共需要 1 亿个 bit,内存占用约 100M。

**思路如下**

申请一个位图数组,长度为 1 亿,初始化为 0。然后遍历所有电话号码,把号码对应的位图中的位置置为 1。遍历完成后,如果 bit 为 1,则表示这个电话号码在文件中存在,否则不存在。bit 值为 1 的数量即为 不同电话号码的个数。

### 方法总结

求解数据重复问题,记得考虑位图法。
20 changes: 20 additions & 0 deletions docs/big-data/find-a-number-if-exists.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## 如何在大量的数据中判断一个数是否存在?

### 题目描述

给定 40 亿个不重复的没排过序的 unsigned int 型整数,然后再给定一个数,如何快速判断这个数是否在这 40 亿个整数当中?

### 解答思路

#### 方法一:分治法
依然可以用分治法解决,方法与前面类似,就不再次赘述了。

#### 方法二:位图法

40 亿个不重复整数,我们用 40 亿个 bit 来表示,初始位均为 0,那么总共需要内存:4, 000, 000, 000b≈512M。

我们读取这 40 亿个整数,将对应的 bit 设置为 1。接着读取要查询的数,查看相应位是否为 1,如果为 1 表示存在,如果为 0 表示不存在。

### 方法总结

**判断数字是否存在、判断数字是否重复的问题**,位图法是一种非常高效的方法。
24 changes: 24 additions & 0 deletions docs/big-data/find-common-urls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## 如何从大量的 URL 中找出相同的 URL?

### 题目描述

给定 a、b 两个文件,各存放 50 亿个 URL,每个 URL 各占 64B,内存限制是 4G。请找出 a、b 两个文件共同的 URL。

### 解答思路

每个 URL 占 64B,那么 50 亿个 URL占用的空间大小约为 320GB。

> 5, 000, 000, 000 * 64B ≈ 5GB * 64 = 320GB
由于内存大小只有 4G,因此,我们不可能一次性把所有 URL 加载到内存中处理。对于这种类型的题目,一般采用**分治策略**,即:把一个文件中的 URL 按照某个特征划分为多个小文件,使得每个小文件大小不超过 4G,这样就可以把这个小文件读到内存中进行处理了。

**思路如下**

首先遍历文件 a,对遍历到的 URL 求 `hash(URL) % 1000` ,根据计算结果把遍历到的 URL 存储到 a<sub>0</sub>, a<sub>1</sub>, a<sub>2</sub>, ..., a<sub>999</sub>,这样每个大小约为 300MB。使用同样的方法遍历文件 b,把文件 b 中的 URL 分别存储到文件 b<sub>0</sub>, b<sub>1</sub>, b<sub>2</sub>, ..., b<sub>999</sub> 中。这样处理过后,所有可能相同的 URL 都在对应的小文件中,即 a<sub>0</sub> 对应 b<sub>0</sub>, ..., a<sub>999</sub> 对应 b<sub>999</sub>,不对应的小文件不可能有相同的 URL。那么接下来,我们只需要求出这 1000 对小文件中相同的 URL 就好了。

接着遍历 a<sub>i</sub>( `i∈[0,999]` ),把 URL 存储到一个 HashSet 集合中。然后遍历 b<sub>i</sub> 中每个 URL,看在 HashSet 集合中是否存在,若存在,说明这就是共同的 URL,可以把这个 URL 保存到一个单独的文件中。

### 方法总结

1. 分而治之,进行哈希取余;
2. 对每个子文件进行 HashSet 统计。
45 changes: 45 additions & 0 deletions docs/big-data/find-hotest-query-string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## 如何查询最热门的查询串?

### 题目描述

搜索引擎会通过日志文件把用户每次检索使用的所有查询串都记录下来,每个查询串的长度不超过 255 字节。

假设目前有 1000w 个记录(这些查询串的重复度比较高,虽然总数是 1000w,但如果除去重复后,则不超过 300w 个)。请统计最热门的 10 个查询串,要求使用的内存不能超过 1G。(一个查询串的重复度越高,说明查询它的用户越多,也就越热门。)

### 解答思路

每个查询串最长为 255B,1000w 个串需要占用 约 2.55G 内存,因此,我们无法将所有字符串全部读入到内存中处理。

#### 方法一:分治法

分治法依然是一个非常实用的方法。

划分为多个小文件,保证单个小文件中的字符串能被直接加载到内存中处理,然后求出每个文件中出现次数最多的 10 个字符串;最后通过一个小顶堆统计出所有文件中出现最多的 10 个字符串。

方法可行,但不是最好,下面介绍其他方法。

#### 方法二:HashMap 法

虽然字符串总数比较多,但去重后不超过 300w,因此,可以考虑把所有字符串及出现次数保存在一个 HashMap 中,所占用的空间为 300w*(255+4)≈777M(其中,4表示整数占用的4个字节)。由此可见,1G 的内存空间完全够用。

**思路如下**

首先,遍历字符串,若不在 map 中,直接存入 map,value 记为 1;若在 map 中,则把对应的 value 加 1,这一步时间复杂度 `O(N)`

接着遍历 map,构建一个 10 个元素的小顶堆,若遍历到的字符串的出现次数大于堆顶字符串的出现次数,则进行替换,并将堆调整为小顶堆。

遍历结束后,堆中 10 个字符串就是出现次数最多的字符串。这一步时间复杂度 `O(Nlog10)`

#### 方法三:前缀树法

方法二使用了 HashMap 来统计次数,当这些字符串有大量相同前缀时,可以考虑使用前缀树来统计字符串出现的次数,树的结点保存字符串出现次数,0 表示没有出现。

**思路如下**

在遍历字符串时,在前缀树中查找,如果找到,则把结点中保存的字符串次数加 1,否则为这个字符串构建新结点,构建完成后把叶子结点中字符串的出现次数置为 1。

最后依然使用小顶堆来对字符串的出现次数进行排序。

### 方法总结

前缀树经常被用来统计字符串的出现次数。它的另外一个大的用途是字符串查找,判断是否有重复的字符串等。
76 changes: 76 additions & 0 deletions docs/big-data/find-mid-value-in-500-millions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
## 如何从 5 亿个数中找出中位数?

### 题目描述

从 5 亿个数中找出中位数。数据排序后,位置在最中间的数就是中位数。当样本数为奇数时,中位数为 第 `(N+1)/2` 个数;当样本数为偶数时,中位数为 第 `N/2` 个数与第 `1+N/2` 个数的均值。

### 解答思路

如果这道题没有内存大小限制,则可以把所有数读到内存中排序后找出中位数。但是最好的排序算法的时间复杂度都为 `O(NlogN)` 。这里使用其他方法。

#### 方法一:双堆法

维护两个堆,一个大顶堆,一个小顶堆。大顶堆中最大的数**小于等于**小顶堆中最小的数;保证这两个堆中的元素个数的差不超过 1。

若数据总数为**偶数**,当这两个堆建好之后,**中位数就是这两个堆顶元素的平均值**。当数据总数为**奇数**时,根据两个堆的大小,**中位数一定在数据多的堆的堆顶**

``` java
class MedianFinder {

private PriorityQueue<Integer> maxHeap;
private PriorityQueue<Integer> minHeap;

/** initialize your data structure here. */
public MedianFinder() {
maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
minHeap = new PriorityQueue<>(Integer::compareTo);
}

public void addNum(int num) {
if (maxHeap.isEmpty() || maxHeap.peek() > num) {
maxHeap.offer(num);
} else {
minHeap.offer(num);
}

int size1 = maxHeap.size();
int size2 = minHeap.size();
if (size1 - size2 > 1) {
minHeap.offer(maxHeap.poll());
} else if (size2 - size1 > 1) {
maxHeap.offer(minHeap.poll());
}
}

public double findMedian() {
int size1 = maxHeap.size();
int size2 = minHeap.size();

return size1 == size2
? (maxHeap.peek() + minHeap.peek()) * 1.0 / 2
: (size1 > size2 ? maxHeap.peek() : minHeap.peek());
}
}
```

> 见 LeetCode No.295:https://leetcode.com/problems/find-median-from-data-stream/
以上这种方法,需要把所有数据都加载到内存中。当数据量很大时,就不能这样了,因此,这种方法**适用于数据量较小的情况**。5 亿个数,每个数字占用 4B,总共需要 2G 内存。如果可用内存不足 2G,就不能使用这种方法了,下面介绍另一种方法。

#### 方法二:分治法

分治法的思想是把一个大的问题逐渐转换为规模较小的问题来求解。

对于这道题,顺序读取这 5 亿个数字,对于读取到的数字 num,如果它对应的二进制中最高位为 1,则把这个数字写到 f1 中,否则写入 f0 中。通过这一步,可以把这 5 亿个数划分为两部分,而且 f0 中的数都大于 f1 中的数(最高位是符号位)。

划分之后,可以非常容易地知道中位数是在 f0 还是 f1 中。假设 f1 中有 1 亿个数,那么中位数一定在 f0 中,且是在 f0 中,从小到大排列的第 1.5 亿个数与它后面的一个数的平均值。

> **提示**,5 亿数的中位数是第 2.5 亿与右边相邻一个数求平均值。若 f1 有一亿个数,那么中位数就是 f0 中从第 1.5 亿个数开始的两个数求得的平均值。
对于 f0 可以用次高位的二进制继续将文件一分为二,如此划分下去,直到划分后的文件可以被加载到内存中,把数据加载到内存中以后直接排序,找出中位数。

> **注意**,当数据总数为偶数,如果划分后两个文件中的数据有相同个数,那么中位数就是数据较小的文件中的最大值与数据较大的文件中的最小值的平均值。
### 方法总结

分治法,真香!
60 changes: 60 additions & 0 deletions docs/big-data/find-no-repeat-number.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## 如何在大量的数据中找出不重复的整数?

### 题目描述

在 2.5 亿个整数中找出不重复的整数。注意:内存不足以容纳这 2.5 亿个整数。

### 解答思路

#### 方法一:分治法
与前面的题目方法类似,先将 2.5 亿个数划分到多个小文件,用 HashSet/HashMap 找出每个小文件中不重复的整数,再合并每个子结果,即为最终结果。

#### 方法二:位图法

**位图**,就是用一个或多个 bit 来标记某个元素对应的值,而键就是该元素。采用位作为单位来存储数据,可以大大节省存储空间。

位图通过使用位数组来表示某些元素是否存在。它可以用于快速查找,判重,排序等。不是很清楚?我先举个小例子。

假设我们要对 `[0,7]` 中的 5 个元素 (6, 4, 2, 1, 5) 进行排序,可以采用位图法。0~7 范围总共有 8 个数,只需要 8bit,即 1 个字节。首先将每个位都置 0:

```
0 0 0 0 0 0 0 0
```

然后遍历 5 个元素,首先遇到 6,那么将下标为 6 的位的 0 置为 1;接着遇到 4,把下标为 4 的位 的 0 置为 1:

```
0 0 0 0 1 0 1 0
```

依次遍历,结束后,位数组是这样的:

```
0 1 1 0 1 1 1 0
```

每个为 1 的位,它的下标都表示了一个数:

```
for i in range(8):
if bits[i] == 1:
print(i)
```

这样我们其实就已经实现了排序。

对于整数相关的算法的求解,**位图法**是一种非常实用的算法。假设 int 整数占用 4B,即 32bit,那么我们可以表示的整数的个数为 2<sup>32</sup>。

**那么对于这道题**,我们用 2 个 bit 来表示各个数字的状态:

* 00 表示这个数字没出现过;
* 01 表示这个数字出现过一次(即为题目所找的不重复整数);
* 10 表示这个数字出现了多次。

那么这 2<sup>32</sup> 个整数,总共所需内存为 2<sup>32</sup>*2b=1GB。因此,当可用内存超过 1GB 时,可以采用位图法。假设内存满足位图法需求,进行下面的操作:

遍历 2.5 亿个整数,查看位图中对应的位,如果是 00,则变为 01,如果是 01 则变为 10,如果是 10 则保持不变。遍历结束后,查看位图,把对应位是 01 的整数输出即可。

### 方法总结

**判断数字是否重复的问题**,位图法是一种非常高效的方法。
Loading

0 comments on commit 84b0542

Please sign in to comment.