mirror of
https://github.com/Estom/notes.git
synced 2026-04-05 11:57:37 +08:00
算法
This commit is contained in:
@@ -26,14 +26,15 @@
|
||||

|
||||

|
||||
|
||||
主要包括五类
|
||||
1. 构造
|
||||
2. 赋值与交换
|
||||
3. 大小
|
||||
4. 增删查
|
||||
5. 迭代器
|
||||
主要包括六类
|
||||
1. 构造函数和初始化(默认初始化、复制初始化、迭代器初始化、列表初始化)
|
||||
2. 赋值与交换(c1=c2,c1={},assign,swap)
|
||||
3. 容器大小(size,max_size,empty)
|
||||
4. 插入删除(insert,emplace,erase,clear)
|
||||
5. 关系运算符(六种关系)
|
||||
6. 迭代器(八个迭代器begin,end,cbegin,cend,rbegin,rend,crbegin,crend)
|
||||
|
||||
### 容器的定义和初始化
|
||||
### 容器的构造函数和初始化
|
||||
|
||||

|
||||
|
||||
@@ -42,13 +43,17 @@
|
||||

|
||||
|
||||
|
||||
### 容器大小的操作
|
||||
### 容器大小
|
||||
|
||||
* size():返回容器中元素的数目
|
||||
* empty():当size为0是返回true
|
||||
* maxsize():返回容器所能容纳的最大元素数的值。
|
||||
|
||||
|
||||
### 插入删除
|
||||
* insert()插入对象
|
||||
* emplace()元素初始化插入
|
||||
* erase()删除指定元素
|
||||
* clear()清空
|
||||
### 关系运算符
|
||||
|
||||
* 容器支持相等和不等的运算。== !=
|
||||
|
||||
@@ -3,41 +3,45 @@
|
||||
> 目录
|
||||
> * array
|
||||
> * vector
|
||||
> * deque
|
||||
> * deque双端队列
|
||||
> * list
|
||||
> * forward_list
|
||||
> * string//专门用于字符串访问的容器
|
||||
|
||||
> * vector/deque/list拥有容器所有的操作。首尾相关的操作。
|
||||
## 0 顺序容器的基础操作
|
||||
|
||||
### 向顺序容器中添加元素
|
||||
|
||||

|
||||
|
||||
* 在尾部添加元素push_back()
|
||||
* 在头部添加元素push_front()
|
||||
* 在中间添加元素insert()
|
||||
* 在尾部添加元素**push_back(),emplace_back()**
|
||||
* 在头部添加元素**push_front(),emplace_front()**
|
||||
* 在中间添加元素insert(),emplace()
|
||||
|
||||
|
||||
### 在顺序容器中访问元素
|
||||
|
||||

|
||||
|
||||
* 也可以使用迭代器访问元素。
|
||||
* at会进行安全检查抛出异常。[]不会进行检查。
|
||||
* 也可以使用**迭代器**访问元素。
|
||||
* **at**会进行安全检查抛出异常。
|
||||
* **[]下标运算符**不会进行检查。
|
||||
* **back(),front()**
|
||||
|
||||
### 在顺序容器中删除元素
|
||||
|
||||

|
||||
|
||||
* pop_back(),pop_front();
|
||||
* erease(p),erase(b,e);
|
||||
* clear();
|
||||
|
||||
> 操作记忆
|
||||
> * back、front、push_back、push_front、pop_back、pop_front、emplace_front、emplace_back。是一组首尾相关的插入操作。
|
||||
> * insert、at、erase。是一组随机的操作。
|
||||
> * **back、front、push_back、push_front、pop_back、pop_front、emplace_front、emplace_back**。是一组首尾相关的插入操作。
|
||||
> * **insert、at、erase**。是一组随机的操作。
|
||||
|
||||
|
||||
### foward_list的特殊操作
|
||||
|
||||

|
||||
|
||||
### 改变容器的大小
|
||||
|
||||
@@ -49,7 +53,7 @@
|
||||
> 数组不能copy赋值,但是array可以copy赋值。
|
||||
|
||||
### 定义
|
||||
|
||||
是静态的连续数组,只有默认初始化。
|
||||
```
|
||||
array<int, 5> arr = {1, 2, 3, 4, 5};
|
||||
```
|
||||
@@ -59,5 +63,7 @@ array<int, 5> arr = {1, 2, 3, 4, 5};
|
||||
## 3 deque
|
||||
|
||||
## 4 foward_list
|
||||
### foward_list的特殊操作
|
||||
|
||||

|
||||
## 5 list
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
### 容器适配器的操作
|
||||
|
||||

|
||||
* 可以用顺序容器初始化适配器。使用的是顺序容器的拷贝。
|
||||
|
||||
|
||||
## 1 stack
|
||||
|
||||
* 默认基于deque实现,也可以基于list/vector
|
||||
### 概念
|
||||
|
||||
### 特有操作
|
||||
@@ -29,7 +30,8 @@
|
||||
## 2 queue和priority_queue
|
||||
|
||||
### 概念
|
||||
|
||||
* queue是基于deque实现的,也可以用vector或list
|
||||
* priority_queue是基于vector实现的。也可以用deque实现
|
||||
|
||||
### 特有操作
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ int main(){
|
||||
|
||||
//stack test
|
||||
deque<int> deq{2,3,4,5};
|
||||
|
||||
stack<int> stk{deq};
|
||||
int m = stk.top();
|
||||
stk.pop();
|
||||
@@ -17,6 +18,6 @@ int main(){
|
||||
cout<<n<<endl;
|
||||
}
|
||||
|
||||
|
||||
vector<int> vec;
|
||||
return 0;
|
||||
}
|
||||
209
C++/标准库/5 字符串.md
209
C++/标准库/5 字符串.md
@@ -3,8 +3,211 @@
|
||||
|
||||
> 参考顺序容器部分
|
||||
|
||||
## 2 string字符串操作
|
||||
|
||||
## 2 string字符串的操作
|
||||
## 2.1 构造函数
|
||||
* string 类有多个构造函数,用法示例如下:
|
||||
```C++
|
||||
string s1(); // si = ""
|
||||
string s2("Hello"); // s2 = "Hello"
|
||||
string s3(4, 'K'); // s3 = "KKKK"
|
||||
string s4("12345", 1, 3); //s4 = "234",即 "12345" 的从下标 1 开始,长度为 3 的子串
|
||||
```
|
||||
* 为称呼方便,本教程后文将从字符串下标 n 开始、长度为 m 的字符串称为“子串(n, m)”。
|
||||
* string 类没有接收一个整型参数或一个字符型参数的构造函数。下面的两种写法是错误的:
|
||||
|
||||
```C++
|
||||
string s1('K');
|
||||
string s2(123);
|
||||
```
|
||||
## 2.2 对 string 对象赋值
|
||||
* 可以用 char* 类型的变量、常量,以及 char 类型的变量、常量对 string 对象进行赋值。例如:
|
||||
```C++
|
||||
string s1;
|
||||
s1 = "Hello"; // s1 = "Hello"
|
||||
s2 = 'K'; // s2 = "K”
|
||||
```
|
||||
* string 类还有 assign 成员函数,可以用来对 string 对象赋值。assign 成员函数返回对象自身的引用。例如:
|
||||
```C++
|
||||
string s1("12345"), s2;
|
||||
s3.assign(s1); // s3 = s1
|
||||
s2.assign(s1, 1, 2); // s2 = "23",即 s1 的子串(1, 2)
|
||||
s2.assign(4, 'K'); // s2 = "KKKK"
|
||||
s2.assign("abcde", 2, 3); // s2 = "cde",即 "abcde" 的子串(2, 3)
|
||||
```
|
||||
## 2.3 求字符串的长度
|
||||
* length 成员函数返回字符串的长度。size 成员函数可以实现同样的功能。
|
||||
## 2.4 string对象中字符串的连接
|
||||
* 除了可以使用+和+=运算符对 string 对象执行字符串的连接操作外,string 类还有 append 成员函数,可以用来向字符串后面添加内容。append 成员函数返回对象自身的引用。例如:
|
||||
|
||||
```C++
|
||||
string s1("123"), s2("abc");
|
||||
s1.append(s2); // s1 = "123abc"
|
||||
s1.append(s2, 1, 2); // s1 = "123abcbc"
|
||||
s1.append(3, 'K'); // s1 = "123abcbcKKK"
|
||||
s1.append("ABCDE", 2, 3); // s1 = "123abcbcKKKCDE",添加 "ABCDE" 的子串(2, 3)
|
||||
```
|
||||
## 2.5 string对象的比较
|
||||
* 除了可以用 <、<=、==、!=、>=、> 运算符比较 string 对象外,string 类还有 compare 成员函数,可用于比较字符串。compare 成员函数有以下返回值:
|
||||
* 小于 0 表示当前的字符串小;
|
||||
* 等于 0 表示两个字符串相等;
|
||||
* 大于 0 表示另一个字符串小。
|
||||
|
||||
例如:
|
||||
```C++
|
||||
string s1("hello"), s2("hello, world");
|
||||
int n = s1.compare(s2);
|
||||
n = s1.compare(1, 2, s2, 0, 3); //比较s1的子串 (1,2) 和s2的子串 (0,3)
|
||||
n = s1.compare(0, 2, s2); // 比较s1的子串 (0,2) 和 s2
|
||||
n = s1.compare("Hello");
|
||||
n = s1.compare(1, 2, "Hello"); //比较 s1 的子串(1,2)和"Hello”
|
||||
n = s1.compare(1, 2, "Hello", 1, 2); //比较 s1 的子串(1,2)和 "Hello" 的子串(1,2)
|
||||
```
|
||||
## 2.6 求 string 对象的子串
|
||||
* substr 成员函数可以用于求子串 (n, m),原型如下:
|
||||
|
||||
```C++
|
||||
string substr(int n = 0, int m = string::npos) const;
|
||||
```
|
||||
* 调用时,如果省略 m 或 m 超过了字符串的长度,则求出来的子串就是从下标 n 开始一直到字符串结束的部分。例如:
|
||||
|
||||
```C++
|
||||
string s1 = "this is ok";
|
||||
string s2 = s1.substr(2, 4); // s2 = "is i"
|
||||
s2 = s1.substr(2); // s2 = "is is ok"
|
||||
```
|
||||
## 2.7 交换两个string对象的内容
|
||||
|
||||
* swap 成员函数可以交换两个 string 对象的内容。例如:
|
||||
```C++
|
||||
string s1("West”), s2("East");
|
||||
s1.swap(s2); // s1 = "East",s2 = "West"
|
||||
```
|
||||
|
||||
## 2.8 查找子串和字符
|
||||
* string 类有一些查找子串和字符的成员函数,它们的返回值都是子串或字符在 string 对象字符串中的位置(即下标)。如果查不到,则返回 string::npos。string: :npos 是在 string 类中定义的一个静态常量。这些函数如下:
|
||||
|
||||
|
||||
* find:从前往后查找子串或字符出现的位置。
|
||||
* rfind:从后往前查找子串或字符出现的位置。
|
||||
* find_first_of:从前往后查找何处出现另一个字符串中包含的字符。例如:
|
||||
|
||||
```C++
|
||||
s1.find_first_of("abc"); //查找s1中第一次出现"abc"中任一字符的位置
|
||||
```
|
||||
find_last_of:从后往前查找何处出现另一个字符串中包含的字符。
|
||||
find_first_not_of:从前往后查找何处出现另一个字符串中没有包含的字符。
|
||||
find_last_not_of:从后往前查找何处出现另一个字符串中没有包含的字符。
|
||||
|
||||
* 下面是 string 类的查找成员函数的示例程序。
|
||||
```C++
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
using namespace std;
|
||||
int main()
|
||||
{
|
||||
string s1("Source Code");
|
||||
int n;
|
||||
if ((n = s1.find('u')) != string::npos) //查找 u 出现的位置
|
||||
cout << "1) " << n << "," << s1.substr(n) << endl;
|
||||
//输出 l)2,urce Code
|
||||
if ((n = s1.find("Source", 3)) == string::npos)
|
||||
//从下标3开始查找"Source",找不到
|
||||
cout << "2) " << "Not Found" << endl; //输出 2) Not Found
|
||||
if ((n = s1.find("Co")) != string::npos)
|
||||
//查找子串"Co"。能找到,返回"Co"的位置
|
||||
cout << "3) " << n << ", " << s1.substr(n) << endl;
|
||||
//输出 3) 7, Code
|
||||
if ((n = s1.find_first_of("ceo")) != string::npos)
|
||||
//查找第一次出现或 'c'、'e'或'o'的位置
|
||||
cout << "4) " << n << ", " << s1.substr(n) << endl;
|
||||
//输出 4) l, ource Code
|
||||
if ((n = s1.find_last_of('e')) != string::npos)
|
||||
//查找最后一个 'e' 的位置
|
||||
cout << "5) " << n << ", " << s1.substr(n) << endl; //输出 5) 10, e
|
||||
if ((n = s1.find_first_not_of("eou", 1)) != string::npos)
|
||||
//从下标1开始查找第一次出现非 'e'、'o' 或 'u' 字符的位置
|
||||
cout << "6) " << n << ", " << s1.substr(n) << endl;
|
||||
//输出 6) 3, rce Code
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
## 2.9 替换子串
|
||||
* replace 成员函数可以对 string 对象中的子串进行替换,返回值为对象自身的引用。例如:
|
||||
|
||||
```C++
|
||||
string s1("Real Steel");
|
||||
s1.replace(1, 3, "123456", 2, 4); //用 "123456" 的子串(2,4) 替换 s1 的子串(1,3)
|
||||
cout << s1 << endl; //输出 R3456 Steel
|
||||
string s2("Harry Potter");
|
||||
s2.replace(2, 3, 5, '0'); //用 5 个 '0' 替换子串(2,3)
|
||||
cout << s2 << endl; //输出 HaOOOOO Potter
|
||||
int n = s2.find("OOOOO"); //查找子串 "00000" 的位置,n=2
|
||||
s2.replace(n, 5, "XXX"); //将子串(n,5)替换为"XXX"
|
||||
cout << s2 < < endl; //输出 HaXXX Potter
|
||||
```
|
||||
## 2.10 删除子串
|
||||
* erase 成员函数可以删除 string 对象中的子串,返回值为对象自身的引用。例如:
|
||||
|
||||
```C++
|
||||
string s1("Real Steel");
|
||||
s1.erase(1, 3); //删除子串(1, 3),此后 s1 = "R Steel"
|
||||
s1.erase(5); //删除下标5及其后面的所有字符,此后 s1 = "R Ste"
|
||||
```
|
||||
## 2.11 插入字符串
|
||||
* insert 成员函数可以在 string 对象中插入另一个字符串,返回值为对象自身的引用。例如:
|
||||
|
||||
```C++
|
||||
string s1("Limitless"), s2("00");
|
||||
s1.insert(2, "123"); //在下标 2 处插入字符串"123",s1 = "Li123mitless"
|
||||
s1.insert(3, s2); //在下标 2 处插入 s2 , s1 = "Li10023mitless"
|
||||
s1.insert(3, 5, 'X'); //在下标 3 处插入 5 个 'X',s1 = "Li1XXXXX0023mitless"
|
||||
```
|
||||
## 2.12 将 string 对象作为流处理
|
||||
* 使用流对象 istringstream 和 ostringstream,可以将 string 对象当作一个流进行输入输出。使用这两个类需要包含头文件 sstream。
|
||||
|
||||
* 示例程序如下:
|
||||
|
||||
```C++
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
using namespace std;
|
||||
int main()
|
||||
{
|
||||
string src("Avatar 123 5.2 Titanic K");
|
||||
istringstream istrStream(src); //建立src到istrStream的联系
|
||||
string s1, s2;
|
||||
int n; double d; char c;
|
||||
istrStream >> s1 >> n >> d >> s2 >> c; //把src的内容当做输入流进行读取
|
||||
ostringstream ostrStream;
|
||||
ostrStream << s1 << endl << s2 << endl << n << endl << d << endl << c <<endl;
|
||||
cout << ostrStream.str();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
## 2.13 用 STL 算法操作 string 对象
|
||||
* string 对象也可以看作一个顺序容器,它支持随机访问迭代器,也有 begin 和 end 等成员函数。STL 中的许多算法也适用于 string 对象。下面是用 STL 算法操作 string 对象的程序示例。
|
||||
|
||||
```C++
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
using namespace std;
|
||||
int main()
|
||||
{
|
||||
string s("afgcbed");
|
||||
string::iterator p = find(s.begin(), s.end(), 'c');
|
||||
if (p!= s.end())
|
||||
cout << p - s.begin() << endl; //输出 3
|
||||
sort(s.begin(), s.end());
|
||||
cout << s << endl; //输出 abcdefg
|
||||
next_permutation(s.begin(), s.end());
|
||||
cout << s << endl; //输出 abcdegf
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
## 3 string字符串的操作
|
||||
|
||||
### string的构造方法
|
||||
|
||||
@@ -47,11 +250,11 @@ string s("helloworld");
|
||||
const char * str = s.c_str();
|
||||
```
|
||||
|
||||
## 3 string相关的外部算法
|
||||
## 4 string相关的外部算法
|
||||
|
||||
> string因为支持迭代器,所以支持所有的容器模板算法。
|
||||
|
||||
|
||||
## 4 正则匹配
|
||||
## 5 正则匹配
|
||||
|
||||
> 在正则表达式部分,有专门针对string的正则匹配搜索算法。
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
## 1 基本概念
|
||||
|
||||
### 定义
|
||||
算法是一系列解决问题的清晰指令,对于符合一定规范的输入,算法能够在有限时间内获得所要求的输出。算法是解决问题的一种方法或过程,它是由若干条指令组成的有穷序列。
|
||||
* 算法是一系列解决问题的清晰指令,对于符合一定规范的输入,算法能够在有限时间内获得所要求的输出。算法是解决问题的一种方法或过程,它是由若干条指令组成的有穷序列。
|
||||
* 算法本质上不是数学,而是逻辑。
|
||||
|
||||
### 特征
|
||||
* 输入:有零或多个外部量作为算法的输入。
|
||||
@@ -26,18 +27,18 @@
|
||||
|
||||
1. **理解问题**(确定**问题抽象**(将应用问题抽象为数学问题)、**问题分类**(搜索、排序。))
|
||||
2. **选择策略**(选择合适的**数据结构**、选择合适的**算法思想**)
|
||||
3. **算法设计**(设计**算法流程**,伪代码)
|
||||
3. **算法设计**(确定**算法技术**,设计**算法流程**,数学递推关系和伪代码)
|
||||
4. **正确性证明**(查看伪代码的流程的正确性,**算法特例**)
|
||||
5. **算法分析**(分析算法**执行效率**)
|
||||
6. **程序设计**(编程)
|
||||
6. **程序设计**(**编程**)
|
||||
|
||||
### 更详细的说明
|
||||
|
||||
> 自己在处理一个题之前到底应该做哪些事情,或者说,按照怎样的流程?是否应该把这些流程写下来?或者按照某种套路来,将会事半功倍。
|
||||
|
||||
* 理解问题、问题分析,问题抽象和问题分类。应该将一个问题,归为某一个类别。问题类别,应该属于某个算法思想下的。也就是说,一个问题类别,应该用某种算法思想来解决。但是两者并不是完全重合。
|
||||
* 选择策略,主要是选择使用哪种算法思想。当确定了问题的类别之后。就可以确定其算法思想了。或者说,算法四线和问题类别应该是同时确定的。接下来需要做的是,设计一些静态的东西。例如数据结构等。
|
||||
* 算法设计,在静态结构上添加流程。
|
||||
* 选择策略,主要是选择使用哪种算法思想(包括蛮力法、递归与分治、动态规划、贪心、回溯、分支限界思想)。当确定了问题的类别之后。就可以确定其算法思想了。或者说,算法思想和问题类别应该是同时确定的。接下来需要做的是,设计一些静态的东西。例如数据结构等。
|
||||
* 算法设计,每一类算法思想都有固定的算法技术,比如分治算法思想,对应的递归求解技术。例如回溯法和蛮力法的深度优先搜索思想,使用的是递归的算法技术和栈+循环的算法技术。广度优先搜索或者分治限界的算法思想,对应的是队列+循环的技术。
|
||||
* 正确性证明,主要分析特例和为考虑到的特殊情况。尽可能举反例,同时完善设计好的算法。
|
||||
* 算法分析,主要分析算法的执行效率上的可行性。
|
||||
* 程序设计
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
## 1 近似算法
|
||||
|
||||
### 概念
|
||||
不同的近似算法有各种各样的复杂度,但其中许多算法都是基于特定问题的直观推断贪婪算法。直观推断是一种来自于经验而不是来自于数学证明的常识性规则。如果我们使用的算法所给出的输出仅仅是实际最优解的一个逼近,我们就会想知道这个逼近有多精确。
|
||||
* 不同的近似算法有各种各样的复杂度,但其中许多算法都是基于特定问题的直观推断贪婪算法。直观推断是一种来自于经验而不是来自于数学证明的常识性规则。如果我们使用的算法所给出的输出仅仅是实际最优解的一个逼近,我们就会想知道这个逼近有多精确。
|
||||
|
||||
|
||||
### 近似算法精度
|
||||
|
||||
对于一个对某些函数 f 最小化的问题来说,可以用近似解的相对误差规模
|
||||
* 对于一个对某些函数 f 最小化的问题来说,可以用近似解的相对误差规模
|
||||
$$
|
||||
re(S_a)=\frac{f(S_*)}{f(S^a)}
|
||||
$$
|
||||
|
||||
### 近似算法的性能
|
||||
|
||||
对于问题的所有实例,它们可能的r(sa)的最佳(也就是最低)上界,被称为该算法的性能比,计作RA。
|
||||
性能比是一个来指出近似算法质量的主要指标,我们需要那些RA尽量接近1的近似算法。
|
||||
* 对于问题的所有实例,它们可能的r(sa)的最佳(也就是最低)上界,被称为该算法的性能比,计作RA。
|
||||
* 性能比是一个来指出近似算法质量的主要指标,我们需要那些RA尽量接近1的近似算法。
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
### 算法原理
|
||||
|
||||
利用邻域结构进行逐步优化的局部搜索算法:
|
||||
* 利用邻域结构进行逐步优化的局部搜索算法:
|
||||
|
||||
算法从一初始可行解 s 出发,利用状态发生器持续地在s 的领域中搜索更好的解,若能找到更优解,则以其替代s 成为新的当前解,然后重复上述过程,直至终止条件满足。
|
||||
* 算法从一初始可行解 s 出发,利用状态发生器持续地在s 的领域中搜索更好的解,若能找到更优解,则以其替代s 成为新的当前解,然后重复上述过程,直至终止条件满足。
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
### 算法概述
|
||||
|
||||
禁忌搜索(TS)是对局部邻域搜索的一种扩展,是一种全局优化算法。TS算法通过引入一个禁忌表和相应的禁忌准则来避免局部迂回,并通过“渴望准则”来挽救某些被禁忌的相对优化解,进而保证全局的有效搜索以实现全局优化。
|
||||
* 禁忌搜索(TS)是对局部邻域搜索的一种扩展,是一种全局优化算法。TS算法通过引入一个禁忌表和相应的禁忌准则来避免局部迂回,并通过“渴望准则”来挽救某些被禁忌的相对优化解,进而保证全局的有效搜索以实现全局优化。
|
||||
|
||||
标记对应已搜索到的局部最优解的一些对象,并在进一步的迭代搜索中尽量避开这些对象,但不是绝对禁止循环,从而保证对不同的有效搜索途径的探索。
|
||||
* 标记对应已搜索到的局部最优解的一些对象,并在进一步的迭代搜索中尽量避开这些对象,但不是绝对禁止循环,从而保证对不同的有效搜索途径的探索。
|
||||
|
||||
### 基本思想
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ $$
|
||||
1. 设f,g,h 是定义域为自然数集N上的函数,若对某个其它的函数h, 我们有f =O(h)和g=O(h),那么f+g = O(h).
|
||||
2. 假设f 和g是定义域为自然数集合的函数,且满足g=O(f),那么f+g=Θ(f).
|
||||
|
||||
### 常见函数的届
|
||||
### 常见函数的界
|
||||
$$
|
||||
\log_2n=o(\sqrt{n})\\
|
||||
\log_an=Θ(log_bn)\\
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 蛮力法
|
||||
## 1 蛮力法概述
|
||||
蛮力法是一种简单直接地解决问题的方法,常常直接 基于问题的描述和所涉及的概念定义。
|
||||
* 蛮力法是一种简单直接地解决问题的方法,常常直接 基于问题的描述和所涉及的概念定义。
|
||||
|
||||
## 2 排序问题
|
||||
(主要描述解决问题的步骤)
|
||||
@@ -8,10 +8,10 @@
|
||||
* 问题:给定一个可排序的n个元素序列(数字、字符或字符串),对它们按照非降序方式重新排列。
|
||||
|
||||
### 选择策略
|
||||
思想:首先扫描整个序列,找到其中一个最小元素,然后和第一个元素交换,将最小元素归位。然后从第二个元素开始扫描序列,找到后n-1个元素中的一个最小元素,然后和第二个元素交换,将第二小元素归位。进行n-1遍扫描之后,排序完成。
|
||||
* 思想:首先扫描整个序列,找到其中一个最小元素,然后和第一个元素交换,将最小元素归位。然后从第二个元素开始扫描序列,找到后n-1个元素中的一个最小元素,然后和第二个元素交换,将第二小元素归位。进行n-1遍扫描之后,排序完成。
|
||||
### 算法设计
|
||||
|
||||
算法 selectSort(A[n])
|
||||
* 算法 selectSort(A[n])
|
||||
```
|
||||
//用选择法对给定数组排序
|
||||
//输入:一个可排序数组A[0..n-1]
|
||||
@@ -28,8 +28,7 @@ for i←0 to n-2 do
|
||||
* 输入规模:序列元素个数n
|
||||
* 基本操作:比较次数A[j] < A[min]
|
||||
* 影响操作执行的其他因素:n
|
||||
* 构建基本操作的求和表达式:
|
||||
利用求和公式分析算法的时间复杂度:
|
||||
* 构建基本操作的求和表达式:利用求和公式分析算法的时间复杂度:
|
||||

|
||||
|
||||
### 程序设计
|
||||
@@ -37,10 +36,10 @@ for i←0 to n-2 do
|
||||
## 3 顺序查找问题
|
||||
(主要是分析解决问题的步骤)
|
||||
### 理解问题
|
||||
思想:查找键与表中元素从头至尾逐个比较。
|
||||
结果:找到 或 失败
|
||||
限位器:把查找键添加到列表末尾—— 一定成功,避免每次循环时对检查是否越界(边界检查)
|
||||
选择策略
|
||||
* 思想:查找键与表中元素从头至尾逐个比较。
|
||||
* 结果:找到 或 失败
|
||||
* 限位器:把查找键添加到列表末尾—— 一定成功,避免每次循环时对检查是否越界(边界检查)
|
||||
* 选择策略
|
||||
|
||||
### 算法设计
|
||||
|
||||
@@ -60,11 +59,11 @@ for i←0 to n-2 do
|
||||
|
||||
## 4 字符串匹配问题
|
||||
### 理解问题
|
||||
问题:给定一个n个字符组成的串,称为文本,一个m(m≤n)个字符组成的串称为模式,从文本中寻找匹配模式的子串。
|
||||
* 问题:给定一个n个字符组成的串,称为文本,一个m(m≤n)个字符组成的串称为模式,从文本中寻找匹配模式的子串。
|
||||
### 选择策略
|
||||
思想:将模式对准文本的前m个字符,然后从左到右匹配每一对相应的字符,若遇到一对不匹配字符,模式向右移一位,重新开始匹配;若m对字符全部匹配,算法可以停止。注意,在文本中,最后一轮子串匹配的起始位置是n-m(假设文本的下标从0到n-1)
|
||||
* 思想:将模式对准文本的前m个字符,然后从左到右匹配每一对相应的字符,若遇到一对不匹配字符,模式向右移一位,重新开始匹配;若m对字符全部匹配,算法可以停止。注意,在文本中,最后一轮子串匹配的起始位置是n-m(假设文本的下标从0到n-1)
|
||||
### 算法设计
|
||||
算法 bruteForceStringMatch(T[0..n-1],P[0..m-1])
|
||||
* 算法 bruteForceStringMatch(T[0..n-1],P[0..m-1])
|
||||
```
|
||||
//蛮力字符串匹配算法实现
|
||||
//输入1:一个n个字符的数组T[0..n-1]代表一段文本
|
||||
@@ -80,11 +79,11 @@ return -1
|
||||
|
||||
## 5 最近对问题
|
||||
### 理解问题
|
||||
找出一个包含n个点的集合中距离最近的两个点。
|
||||
* 找出一个包含n个点的集合中距离最近的两个点。
|
||||
### 选择策略
|
||||
分别计算每一点对之间的距离,然后从中找出距离最小的那一对。为了避免同一点对计算两次,可以只考虑i<j的点对(Pi, Pj)
|
||||
* 分别计算每一点对之间的距离,然后从中找出距离最小的那一对。为了避免同一点对计算两次,可以只考虑i<j的点对(Pi, Pj)
|
||||
### 算法设计
|
||||
算法 bruteForceClosesPoints(P)
|
||||
* 算法 bruteForceClosesPoints(P)
|
||||
```
|
||||
//蛮力法求解平面中距离最近的两点
|
||||
//输入:一个n(n≥2)个点的列表P,P1=(x1, y1),…,Pn=(xn, yn)
|
||||
@@ -99,5 +98,5 @@ return index1,index2
|
||||
```
|
||||
### 正确性证明
|
||||
### 算法分析
|
||||
O(n2)
|
||||
$O(n^2)$
|
||||
### 程序设计
|
||||
|
||||
@@ -9,11 +9,16 @@
|
||||
7. 哈希查找
|
||||
|
||||
## 0 概述
|
||||
查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文简单概括性的介绍了常见的七种查找算法,说是七种,其实二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。插值查找和斐波那契查找是在二分查找的基础上的优化查找算法。树表查找和哈希查找会在后续的博文中进行详细介绍。
|
||||
* 查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。
|
||||
* 本文简单概括性的介绍了常见的七种查找算法,说是七种,其实二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。
|
||||
* 插值查找和斐波那契查找是在二分查找的基础上的优化查找算法。树表查找和哈希查找会在后续的博文中进行详细介绍。
|
||||
|
||||
> * 查找算法,一般适用于线性结构。
|
||||
> * 搜索算法,一般适用于树和图。
|
||||
|
||||
### 查找定义
|
||||
|
||||
根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
|
||||
* 根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
|
||||
|
||||
### 查找算法分类:
|
||||
1. 静态查找和动态查找;
|
||||
@@ -25,31 +30,31 @@
|
||||
|
||||
### 平均查找长度(Average Search Length,ASL)
|
||||
|
||||
需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。
|
||||
* 需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。
|
||||
|
||||
对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
|
||||
* Pi:查找表中第i个数据元素的概率。
|
||||
* Ci:找到第i个数据元素时已经比较过的次数。
|
||||
*对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
|
||||
* Pi:查找表中第i个数据元素的概率。
|
||||
* Ci:找到第i个数据元素时已经比较过的次数。
|
||||
|
||||
## 1 顺序查找
|
||||
### 说明
|
||||
|
||||
顺序查找适合于存储结构为顺序存储或链接存储的线性表。
|
||||
* 顺序查找适合于存储结构为顺序存储或链接存储的线性表。
|
||||
|
||||
### 基本思想
|
||||
|
||||
顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
|
||||
* 顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
|
||||
|
||||
### 复杂度分析:
|
||||
查找成功时的平均查找长度为:(假设每个数据元素的概率相等)
|
||||
* 查找成功时的平均查找长度为:(假设每个数据元素的概率相等)
|
||||
$$
|
||||
ASL = 1/n(1+2+3+…+n) = (n+1)/2;
|
||||
$$
|
||||
当查找不成功时,需要n+1次比较,时间复杂度为
|
||||
* 当查找不成功时,需要n+1次比较,时间复杂度为
|
||||
$$
|
||||
O(n);
|
||||
$$
|
||||
所以,顺序查找的时间复杂度为O(n)。
|
||||
* 所以,顺序查找的时间复杂度为O(n)。
|
||||
|
||||
### 代码实现
|
||||
```
|
||||
@@ -65,15 +70,15 @@ int SequenceSearch(int a[], int value, int n)
|
||||
```
|
||||
## 2 二分查找
|
||||
### 说明
|
||||
元素必须是有序的,如果是无序的则要先进行排序操作。
|
||||
* 元素必须是有序的,如果是无序的则要先进行排序操作。
|
||||
|
||||
### 基本思想
|
||||
|
||||
也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
|
||||
* 也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
|
||||
|
||||
### 复杂度分析
|
||||
|
||||
最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n);
|
||||
* 最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n);
|
||||
|
||||
> 注:折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。——《大话数据结构》
|
||||
|
||||
@@ -113,23 +118,22 @@ int BinarySearch2(int a[], int value, int low, int high)
|
||||
|
||||
## 3 插值查找
|
||||
### 说明
|
||||
折半查找这种查找方式,不是自适应的(也就是说是傻瓜式的)。二分查找中查找点计算如下:
|
||||
* 折半查找这种查找方式,不是自适应的(也就是说是傻瓜式的)。二分查找中查找点计算如下:
|
||||
$$
|
||||
mid=(low+high)/2, 即mid=low+1/2*(high-low);
|
||||
$$
|
||||
通过类比,我们可以将查找的点改进为如下:
|
||||
* 通过类比,我们可以将查找的点改进为如下:
|
||||
$$
|
||||
mid=low+(key-a[low])/(a[high]-a[low])*(high-low)
|
||||
$$
|
||||
也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。
|
||||
* 也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。假定数据是离散均匀分布的,搜索效率会很高。
|
||||
|
||||
### 基本思想
|
||||
基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。
|
||||
* 基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。
|
||||
> 注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。
|
||||
|
||||
### 复杂度分析
|
||||
查找成功或者失败的时间复杂度均为$O(log_2(log_2n))$。
|
||||
最差时间复杂度O(n)
|
||||
* 查找成功或者失败的时间复杂度均为$O(log_2(log_2n))$。最差时间复杂度O(n)
|
||||
|
||||
### 代码实现
|
||||
```
|
||||
@@ -154,24 +158,24 @@ int InsertionSearch(int a[], int value, int low, int high)
|
||||
> 大家记不记得斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中。
|
||||
|
||||
### 基本思想
|
||||
也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。
|
||||
* 也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。
|
||||
|
||||
相对于折半查找,一般将待比较的key值与第mid=(low+high)/2位置的元素比较,比较结果分三种情况:
|
||||
* 相等,mid位置的元素即为所求
|
||||
* `>,low=mid+1;`
|
||||
* `<,high=mid-1`
|
||||
* 相对于折半查找,一般将待比较的key值与第mid=(low+high)/2位置的元素比较,比较结果分三种情况:
|
||||
* 相等,mid位置的元素即为所求
|
||||
* `>,low=mid+1;`
|
||||
* `<,high=mid-1`
|
||||
|
||||
斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数减1,即n=F(k)-1;
|
||||
开始将k值与第F(k-1)位置的记录进行比较(mid=low+F(k-1)-1),比较结果也分为三种
|
||||
* 斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数减1,即n=F(k)-1;
|
||||
* 开始将k值与第F(k-1)位置的记录进行比较(mid=low+F(k-1)-1),比较结果也分为三种
|
||||
|
||||
* 相等,mid位置的元素即为所求
|
||||
* `>,low=mid+1,k-=2;`
|
||||
* 相等,mid位置的元素即为所求
|
||||
* `>,low=mid+1,k-=2;`
|
||||
|
||||
说明:low=mid+1说明待查找的元素在[mid+1,high]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找。
|
||||
|
||||
* `<,high=mid-1,k-=1`
|
||||
|
||||
说明:low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归 的应用斐波那契查找。
|
||||
* `<,high=mid-1,k-=1`
|
||||
|
||||
说明:low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归 的应用斐波那契查找。
|
||||
|
||||
|
||||
### 复杂度分析
|
||||
@@ -269,35 +273,35 @@ int main()
|
||||
|
||||
### 基本思想
|
||||
|
||||
二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在和每个节点的父节点比较大小,查找最适合的范围。这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
|
||||
* 二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在和每个节点的父节点比较大小,查找最适合的范围。这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
|
||||
|
||||
二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:
|
||||
1. 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
|
||||
2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
|
||||
3. 任意节点的左、右子树也分别为二叉查找树。
|
||||
* 二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:
|
||||
1. 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
|
||||
2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
|
||||
3. 任意节点的左、右子树也分别为二叉查找树。
|
||||
|
||||
二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的数列。
|
||||
* 二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的数列。
|
||||
|
||||
不同形态的二叉查找树如下图所示:
|
||||
* 不同形态的二叉查找树如下图所示:
|
||||

|
||||
|
||||
### 复杂度分析
|
||||
|
||||
它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。
|
||||
* 它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。
|
||||
|
||||
原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
|
||||
* 原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。
|
||||
|
||||
下图为二叉树查找和顺序查找以及二分查找性能的对比图:
|
||||
* 下图为二叉树查找和顺序查找以及二分查找性能的对比图:
|
||||

|
||||
|
||||
## 5.2 平衡查找树之2-3查找树(2-3 Tree)
|
||||
|
||||
### 基本思想
|
||||
2-3查找树定义:和二叉树不一样,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个子节点。对应3节点(3-node),保存两个Key,2-3查找树的定义如下:
|
||||
* 2-3查找树定义:和二叉树不一样,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个子节点。对应3节点(3-node),保存两个Key,2-3查找树的定义如下:
|
||||
|
||||
1. 要么为空,要么:
|
||||
2. 对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key要小,右节点也是一个2-3节点,所有的值比key要大。
|
||||
3. 对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。
|
||||
1. 要么为空,要么:
|
||||
2. 对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key要小,右节点也是一个2-3节点,所有的值比key要大。
|
||||
3. 对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。
|
||||
|
||||

|
||||
|
||||
@@ -317,77 +321,73 @@ int main()
|
||||

|
||||
|
||||
## 5.3 平衡查找树之红黑树(Red-Black Tree)
|
||||
2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgn,从而保证了最坏情况下的时间复杂度。但是2-3树实现起来比较复杂,于是就有了一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree)。
|
||||
* 2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgn,从而保证了最坏情况下的时间复杂度。但是2-3树实现起来比较复杂,于是就有了一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree)。
|
||||
|
||||
### 基本思想
|
||||
红黑树首先是一种树形结构,同时又是一个二叉树(每个节点最多只能有两个孩子节点,左节点小于等于父节点,右节点大于父节点),为了保证树的左右孩子树相对平衡(深度相同),红黑树使用了节点标色的方式,将节点标记为红色或者黑色,在计算树的深度时只统计黑色节点的数量,不统计红色节点数量。
|
||||
* 红黑树首先是一种树形结构,同时又是一个二叉树(每个节点最多只能有两个孩子节点,左节点小于等于父节点,右节点大于父节点),为了保证树的左右孩子树相对平衡(深度相同),红黑树使用了节点标色的方式,将节点标记为红色或者黑色,在计算树的深度时只统计黑色节点的数量,不统计红色节点数量。
|
||||
|
||||
为了保证左右子树的平衡,红黑树定义了一些规则或者特点来维持平衡。
|
||||
* 为了保证左右子树的平衡,红黑树定义了一些规则或者特点来维持平衡。
|
||||
|
||||
主要特点(规则)
|
||||
* 每个节点要么是黑色,要么是红色。(节点非黑即红)
|
||||
* 根节点是黑色。
|
||||
* 每个叶子节点(NULL)是黑色(为了简单期间,一般会省略该节点)。
|
||||
* 如果一个节点是红色的,则它的子节点必须是黑色的。(也就是说父子节点不能同时为红色)
|
||||
* 从一个节点到该节点的每一个叶子子孙节点的所有路径上包含相同数目的黑节点。(这一点是平衡的关键)
|
||||
* 新插入节点默认为红色,插入后需要校验红黑树是否符合规则,不符合则需要进行操作。
|
||||
* 主要特点(规则)
|
||||
* 每个节点要么是黑色,要么是红色。(节点非黑即红)
|
||||
* 根节点是黑色。
|
||||
* 每个叶子节点(NULL)是黑色(为了简单期间,一般会省略该节点)。
|
||||
* 如果一个节点是红色的,则它的子节点必须是黑色的。(也就是说父子节点不能同时为红色)
|
||||
* 从一个节点到该节点的每一个叶子子孙节点的所有路径上包含相同数目的黑节点。(这一点是平衡的关键)
|
||||
* 新插入节点默认为红色,插入后需要校验红黑树是否符合规则,不符合则需要进行操作。
|
||||
|
||||

|
||||
|
||||
|
||||
红黑树平衡方法
|
||||
前面讲到红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色。
|
||||
|
||||
* 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
|
||||
* 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
|
||||
* 变色:结点的颜色由红变黑或由黑变红。
|
||||
* 红黑树平衡方法.前面讲到红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色。
|
||||
* 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
|
||||
* 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
|
||||
* 变色:结点的颜色由红变黑或由黑变红。
|
||||
|
||||
### 复杂度分析
|
||||
最坏的情况就是,红黑相间的路径长度是全黑路径长度的2倍。
|
||||
* 最坏的情况就是,红黑相间的路径长度是全黑路径长度的2倍。
|
||||
红黑树的平均高度大约为2logn。
|
||||
下图是红黑树在各种情况下的时间复杂度,可以看出红黑树是2-3查找树的一种实现,它能保证最坏情况下仍然具有对数的时间复杂度。
|
||||
* 下图是红黑树在各种情况下的时间复杂度,可以看出红黑树是2-3查找树的一种实现,它能保证最坏情况下仍然具有对数的时间复杂度。
|
||||
|
||||

|
||||
|
||||
## 5.4 B树和B+树(B Tree/B+ Tree)
|
||||
|
||||
### 基本思想
|
||||
B树定义:
|
||||
* B树定义:B树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。B树的插入及平衡化操作和2-3树很相似。
|
||||
* 根节点至少有两个子节点
|
||||
* 每个节点有M-1个key,并且以升序排列
|
||||
* 位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
|
||||
* 其它节点至少有M/2个子节点
|
||||
|
||||
B树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。B树的插入及平衡化操作和2-3树很相似。
|
||||
* 根节点至少有两个子节点
|
||||
* 每个节点有M-1个key,并且以升序排列
|
||||
* 位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
|
||||
* 其它节点至少有M/2个子节点
|
||||
|
||||
下图是一个M=4 阶的B树:
|
||||
* 下图是一个M=4 阶的B树:
|
||||

|
||||
|
||||
|
||||
B+树定义:
|
||||
|
||||
B+树是对B树的一种变形树,它与B树的差异在于:
|
||||
* 有k个子结点的结点必然有k个关键码;
|
||||
* 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
|
||||
* 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
|
||||
* B+树是对B树的一种变形树,它与B树的差异在于:
|
||||
* 有k个子结点的结点必然有k个关键码;
|
||||
* 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
|
||||
* 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。
|
||||
|
||||
如下图,是一个B+树:
|
||||
* 如下图,是一个B+树:
|
||||

|
||||
|
||||
B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
|
||||
* B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
|
||||
|
||||
B+ 树的优点在于:
|
||||
|
||||
由于B+树在内部节点上不好含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上关联的数据也具有更好的缓存命中率。
|
||||
B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
|
||||
|
||||
但是B树也有优点,其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
|
||||
* B+ 树的优点在于:
|
||||
* 由于B+树在内部节点上不好含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上关联的数据也具有更好的缓存命中率。
|
||||
* B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
|
||||
* 但是B树也有优点
|
||||
* 由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
|
||||
|
||||
## 6 分块查找
|
||||
> 分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
|
||||
|
||||
### 算法思想
|
||||
|
||||
将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
|
||||
* 将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
|
||||
### 算法流程:
|
||||
1. step1 先选取各块中的最大关键字构成一个索引表;
|
||||
2. step2 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。
|
||||
@@ -396,17 +396,17 @@ B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次
|
||||
### 哈希表-哈希函数原理
|
||||
什么是哈希表?
|
||||
|
||||
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。
|
||||
* 我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。
|
||||
|
||||
总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。
|
||||
* 总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。
|
||||
|
||||
什么是哈希函数?
|
||||
* 什么是哈希函数?
|
||||
|
||||
哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
|
||||
* 哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
|
||||
|
||||
### 算法思想
|
||||
|
||||
哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
|
||||
* 哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
|
||||
|
||||
### 算法流程:
|
||||
1. 用给定的哈希函数构造哈希表;
|
||||
@@ -414,16 +414,14 @@ B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次
|
||||
3. 在哈希表的基础上执行哈希查找。
|
||||
|
||||
|
||||
哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。
|
||||
* 哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。
|
||||
|
||||
### 复杂度分析:
|
||||
单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。
|
||||
* 单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。
|
||||
|
||||
使用Hash,我们付出了什么?
|
||||
* 使用Hash,我们付出了什么?
|
||||
|
||||
我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map,也就是我们常说的KV pair,经常使用Python的博友可能更有这种体会。使用map的好处就是,我们在后续处理数据处理时,可以根据数据的key快速的查找到对应的value值。map的本质就是Hash表,那我们在获取了超高查找效率的基础上,我们付出了什么?
|
||||
|
||||
Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。
|
||||
|
||||
Hash算法和其他查找算法的性能对比:
|
||||
* 我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map,也就是我们常说的KV pair,经常使用Python的博友可能更有这种体会。使用map的好处就是,我们在后续处理数据处理时,可以根据数据的key快速的查找到对应的value值。map的本质就是Hash表,那我们在获取了超高查找效率的基础上,我们付出了什么?
|
||||
* Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。
|
||||
* Hash算法和其他查找算法的性能对比:
|
||||

|
||||
|
||||
@@ -3,38 +3,38 @@
|
||||
## 1 概述
|
||||
|
||||
### 特点
|
||||
广度优先搜索(BFS:Breadth-First Search)是一种图搜索策略,其将搜索限制到 2 种操作:
|
||||
* 访问图中的一个节点;
|
||||
* 访问该节点的邻居节点;
|
||||
* 广度优先搜索(BFS:Breadth-First Search)是一种树和图搜索策略,其将搜索限制到 2 种操作:
|
||||
* 访问图中的一个节点;
|
||||
* 访问该节点的邻居节点;
|
||||
|
||||
### 过程
|
||||
|
||||
广度优先搜索(BFS)由 Edward F. Moore 在 1950 年发表,起初被用于在迷宫中寻找最短路径。在 Prim 最小生成树算法和 Dijkstra 单源最短路径算法中,都采用了与广度优先搜索类似的思想。
|
||||
* 广度优先搜索(BFS)由 Edward F. Moore 在 1950 年发表,起初被用于在迷宫中寻找最短路径。在 Prim 最小生成树算法和 Dijkstra 单源最短路径算法中,都采用了与广度优先搜索类似的思想。
|
||||
|
||||
对图的广度优先搜索与对树(Tree)的广度优先遍历(Breadth First Traversal)是类似的,区别在于图中可能存在环,所以可能会遍历到已经遍历的节点。BFD 算法首先会发现和源顶点 s 距离边数为 k 的所有顶点,然后才会发现和 s 距离边数为 k+1 的其他顶点。
|
||||
* 对图的广度优先搜索与对树(Tree)的广度优先遍历(Breadth First Traversal)是类似的,区别在于图中可能存在环,所以可能会遍历到已经遍历的节点。BFD 算法首先会发现和源顶点 s 距离边数为 k 的所有顶点,然后才会发现和 s 距离边数为 k+1 的其他顶点。
|
||||
|
||||

|
||||
|
||||
### 例子
|
||||
例如,下面的图中,从顶点 2 开始遍历,当遍历到顶点 0 时,邻接的顶点为 1 和 2,而顶点 2 已经遍历过,如果不做标记,遍历过程将陷入死循环。所以,在 BFS 的算法实现中需要对顶点是否访问过做标记。
|
||||
* 例如,下面的图中,从顶点 2 开始遍历,当遍历到顶点 0 时,邻接的顶点为 1 和 2,而顶点 2 已经遍历过,如果不做标记,遍历过程将陷入死循环。所以,在 BFS 的算法实现中需要对顶点是否访问过做标记。
|
||||
|
||||

|
||||
|
||||
上图的 BFS 遍历结果为 [ 2, 0, 3, 1 ]。
|
||||
* 上图的 BFS 遍历结果为 [ 2, 0, 3, 1 ]。
|
||||
|
||||
### 实现
|
||||
BFS 算法的实现通常使用队列(Queue)数据结构来存储遍历图中节点的中间状态,过程如下:
|
||||
1. 将 root 节点 Enqueue;
|
||||
2. Dequeue 一个节点,并检查该节点:
|
||||
* 如果该节点就是要找的目标节点,则结束遍历,返回结果 "Found";
|
||||
* 否则,Enqueue 所有直接后继子节点(如果节点未被访问过);
|
||||
3. 如果 Queue 为空,并且图中的所有节点都被检查过,仍未找到目标节点,则结束搜索,返回结果 "Not Found";
|
||||
4. 如果 Queue 不为空,重复步骤 2;
|
||||
* BFS 算法的实现通常使用**队列Queue**数据结构来存储遍历图中节点的中间状态,过程如下:
|
||||
1. 将 root 节点 Enqueue;
|
||||
2. Dequeue 一个节点,并检查该节点:
|
||||
* 如果该节点就是要找的目标节点,则结束遍历,返回结果 "Found";
|
||||
* 否则,Enqueue 所有直接后继子节点(如果节点未被访问过);
|
||||
3. 如果 Queue 为空,并且图中的所有节点都被检查过,仍未找到目标节点,则结束搜索,返回结果 "Not Found";
|
||||
4. 如果 Queue 不为空,重复步骤 2;
|
||||
|
||||
|
||||
如果需要记录搜索的轨迹,可以为顶点着色。起初所有顶点为白色,随着搜索的进行变为灰色,然后变成黑色。灰色和黑色顶点都是已发现的顶点。
|
||||
* 如果需要记录搜索的轨迹,可以为顶点着色。起初所有顶点为白色,随着搜索的进行变为灰色,然后变成黑色。灰色和黑色顶点都是已发现的顶点。
|
||||
### 时间复杂度
|
||||
广度优先搜索(BFS)的时间复杂度为 O(V+E),V 即 Vertex 顶点数量,E 即 Edge 边数量。
|
||||
* 广度优先搜索(BFS)的时间复杂度为 O(V+E),V 即 Vertex 顶点数量,E 即 Edge 边数量。
|
||||
### BFS算法伪码
|
||||
```
|
||||
1 procedure BFS(G,v) is
|
||||
@@ -61,7 +61,7 @@ BFS 算法的实现通常使用队列(Queue)数据结构来存储遍历图
|
||||
|
||||
|
||||
### BFS算法代码
|
||||
```
|
||||
```java
|
||||
1 using System;
|
||||
2 using System.Collections.Generic;
|
||||
3
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
# 深度优先搜索
|
||||
## 1 概述
|
||||
|
||||
### 特点
|
||||
深度优先搜索(DFS:Depth-First Search)是一种图搜索策略,其将搜索限制到 2 种操作:
|
||||
(a) 访问图中的一个节点;
|
||||
(b) 访问该节点的子节点;
|
||||
### 定义
|
||||
* 深度优先搜索(DFS:Depth-First Search)是一种图搜索策略,其将搜索限制到 2 种操作:
|
||||
1. 访问图中的一个节点;
|
||||
2. 访问该节点的子节点;
|
||||
|
||||
### 过程
|
||||
在深度优先搜索中,对于最新发现的顶点,如果它还有以此为起点而未探测到的边,就沿此边继续探测下去。当顶点 v 的所有边都已被探寻过后,搜索将回溯到发现顶点 v 有起始点的那些边。这一过程一直进行到已发现从源顶点可达的所有顶点为止。实际上深度优先搜索最初的探究也是为了解决迷宫问题。
|
||||
* 在深度优先搜索中,对于最新发现的顶点,如果它还有以此为起点而未探测到的边,就沿此边继续探测下去。当顶点 v 的所有边都已被探寻过后,搜索将回溯到发现顶点 v 有起始点的那些边。这一过程一直进行到已发现从源顶点可达的所有顶点为止。实际上深度优先搜索最初的探究也是为了解决迷宫问题。
|
||||
|
||||
对图的深度优先搜索与对树(Tree)的深度优先遍历(Depth First Traversal)是类似的,区别在于图中可能存在环,所以可能会遍历到已经遍历的节点。
|
||||
* 对图的深度优先搜索与对树(Tree)的深度优先遍历(Depth First Traversal)是类似的,区别在于图中可能存在环,所以可能会遍历到已经遍历的节点。
|
||||
|
||||
### 例子
|
||||
|
||||
例如,下面的图中,从顶点 2 开始遍历,当遍历到顶点 0 时,子顶点为 1 和 2,而顶点 2 已经遍历过,如果不做标记,遍历过程将陷入死循环。所以,在 DFS 的算法实现中需要对顶点是否访问过做标记。
|
||||
* 例如,下面的图中,从顶点 2 开始遍历,当遍历到顶点 0 时,子顶点为 1 和 2,而顶点 2 已经遍历过,如果不做标记,遍历过程将陷入死循环。所以,在 DFS 的算法实现中需要对顶点是否访问过做标记。
|
||||

|
||||
上图的 DFS 遍历结果为 2, 0, 1, 3。
|
||||
* 上图的 DFS 遍历结果为 2, 0, 1, 3。
|
||||
|
||||
### 实现
|
||||
DFS 算法可以通过不同方式来实现:
|
||||
|
||||
递归方式
|
||||
|
||||
非递归方式:使用栈(Stack)数据结构来存储遍历图中节点的中间状态;
|
||||
* DFS 算法可以通过不同方式来实现:
|
||||
* 递归方式
|
||||
* 非递归方式:栈(Stack)数据结构来存储遍历图中节点的中间状态;
|
||||
|
||||
### 时间复杂度
|
||||
深度优先搜索(DFS)的时间复杂度为 O(V+E),V 即 Vertex 顶点数量,E 即 Edge 边数量。
|
||||
|
||||
* 深度优先搜索(DFS)的时间复杂度为 O(V+E),V 即 Vertex 顶点数量,E 即 Edge 边数量。
|
||||
|
||||
### DFS算法的递归方式伪码
|
||||
```
|
||||
@@ -49,7 +48,7 @@ DFS 算法可以通过不同方式来实现:
|
||||
```
|
||||
|
||||
### DFS算法实现代码如下:
|
||||
```
|
||||
```java
|
||||
1 using System;
|
||||
2 using System.Linq;
|
||||
3 using System.Collections.Generic;
|
||||
|
||||
@@ -269,13 +269,13 @@
|
||||
|
||||
### Stable 与 Not Stable 的比较
|
||||
|
||||
稳定排序算法会将相等的元素值维持其相对次序。如果一个排序算法是稳定的,当有两个有相等的元素值 R 和 S,且在原本的列表中 R 出现在 S 之前,那么在排序过的列表中 R 也将会是在 S 之前。
|
||||
* 稳定排序算法会将相等的元素值维持其相对次序。如果一个排序算法是稳定的,当有两个有相等的元素值 R 和 S,且在原本的列表中 R 出现在 S 之前,那么在排序过的列表中 R 也将会是在 S 之前。
|
||||
|
||||

|
||||
|
||||
### O(n2) 与 O(n*logn) 的比较
|
||||
|
||||
合并排序和堆排序在最坏情况下达到上界 O(n*logn),快速排序在平均情况下达到上界 O(n*logn)。对于比较排序算法,我们都能给出 n 个输入的数值,使算法以 Ω(n*logn) 时间运行。
|
||||
* 合并排序和堆排序在最坏情况下达到上界 $O(n*logn)$,快速排序在平均情况下达到上界 $O(n*logn)$。对于比较排序算法,我们都能给出 n 个输入的数值,使算法以 $Ω(n*logn)$ 时间运行。
|
||||
|
||||
> 注:有关算法复杂度,可参考文章《算法复杂度分析》。有关常用数据结构的复杂度,可参考文章《常用数据结构及复杂度》。
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 线性时间选择算法
|
||||
|
||||
## 1 问题概述
|
||||
在一个由 n 个元素组成的集合中,第 i 个顺序统计量(order statistic)是该集合中第 i 小的元素。也就是说,最小值是第 1 个顺序统计量(i = 1),最大值是第 n 个顺序统计量(i = n)。
|
||||
中位数(median)是它所在集合的中点元素。当 n 为奇数时,中位数是唯一的,出现在 i = (n + 1)/2 处。当 n 为偶数时,存在两个中位数,下中位数 i = n/2 和上中位数 i = n/2 + 1 处。因此,不考虑 n 的奇偶性,中位数总是出现在 i = (n+1)/2 的中位数处。本文中所用的中位数总是指下中位数。
|
||||
* 在一个由 n 个元素组成的集合中,第 i 个顺序统计量(order statistic)是该集合中第 i 小的元素。也就是说,最小值是第 1 个顺序统计量(i = 1),最大值是第 n 个顺序统计量(i = n)。
|
||||
* 中位数(median)是它所在集合的中点元素。当 n 为奇数时,中位数是唯一的,出现在 i = (n + 1)/2 处。当 n 为偶数时,存在两个中位数,下中位数 i = n/2 和上中位数 i = n/2 + 1 处。因此,不考虑 n 的奇偶性,中位数总是出现在 i = (n+1)/2 的中位数处。本文中所用的中位数总是指下中位数。
|
||||
* 选择最大值和最小值
|
||||
* 选择中位数或任意位置值
|
||||
|
||||
|
||||
@@ -25,20 +25,19 @@
|
||||
|
||||
### 基本步骤和算法效率
|
||||
|
||||
字符串匹配算法通常分为两个步骤:预处理(Preprocessing)和匹配(Matching)。所以算法的总运行时间为预处理和匹配的时间的总和。
|
||||
|
||||
* 字符串匹配算法通常分为两个步骤:预处理(Preprocessing)和匹配(Matching)。所以算法的总运行时间为预处理和匹配的时间的总和。
|
||||

|
||||
上图描述了常见字符串匹配算法的预处理和匹配时间。
|
||||
* 上图描述了常见字符串匹配算法的预处理和匹配时间。
|
||||
|
||||
## 1 朴素的字符串匹配算法(Naive String Matching Algorithm)
|
||||
|
||||
### 基本思想
|
||||
朴素的字符串匹配算法又称为暴力匹配算法(Brute Force Algorithm),它的主要特点是:
|
||||
1. 没有预处理阶段;
|
||||
2. 滑动窗口总是后移 1 位;
|
||||
3. 对模式中的字符的比较顺序不限定,可以从前到后,也可以从后到前;
|
||||
4. 匹配阶段需要 O((n - m + 1)m) 的时间复杂度;
|
||||
5. 需要 2n 次的字符比较;
|
||||
* 朴素的字符串匹配算法又称为暴力匹配算法(Brute Force Algorithm),它的主要特点是:
|
||||
1. 没有预处理阶段;
|
||||
2. 滑动窗口总是后移 1 位;
|
||||
3. 对模式中的字符的比较顺序不限定,可以从前到后,也可以从后到前;
|
||||
4. 匹配阶段需要 O((n - m + 1)m) 的时间复杂度;
|
||||
5. 需要 2n 次的字符比较;
|
||||
|
||||
很显然,朴素的字符串匹配算法 NAIVE-STRING-MATCHER 是最原始的算法,它通过使用循环来检查是否在范围 n-m+1 中存在满足条件 P[1..m] = T [s + 1..s + m] 的有效位移 s。
|
||||
|
||||
@@ -53,13 +52,14 @@
|
||||
```
|
||||
|
||||

|
||||
如上图中,对于模式 P = aab 和文本 T = acaabc,将模式 P 沿着 T 从左到右滑动,逐个比较字符以判断模式 P 在文本 T 中是否存在。
|
||||
* 如上图中,对于模式 P = aab 和文本 T = acaabc,将模式 P 沿着 T 从左到右滑动,逐个比较字符以判断模式 P 在文本 T 中是否存在。
|
||||
|
||||
### NAIVE-STRING-MATCHER时间效率
|
||||
可以看出,NAIVE-STRING-MATCHER 没有对模式 P 进行预处理,所以预处理的时间为 0。而匹配的时间在最坏情况下为 Θ((n-m+1)m),如果 m = [n/2],则为 Θ(n2)。
|
||||
|
||||
* 可以看出,NAIVE-STRING-MATCHER 没有对模式 P 进行预处理,所以预处理的时间为 0。而匹配的时间在最坏情况下为 Θ((n-m+1)m),如果 m = [n/2],则为 Θ(n2)。
|
||||
|
||||
### NAIVE-STRING-MATCHER 的代码示例。
|
||||
```
|
||||
```java
|
||||
1 namespace StringMatching
|
||||
2 {
|
||||
3 class Program
|
||||
@@ -197,31 +197,31 @@
|
||||
135 }
|
||||
136 }
|
||||
```
|
||||
上面代码中 TryMatch1 和 TryMatch2 分别使用 for 和 while 循环达到相同效果。
|
||||
* 上面代码中 TryMatch1 和 TryMatch2 分别使用 for 和 while 循环达到相同效果。
|
||||
|
||||
|
||||
## 2 Knuth-Morris-Pratt 字符串匹配算法(即 KMP 算法)
|
||||
|
||||
### 基本思想
|
||||
我们来观察一下朴素的字符串匹配算法的操作过程。如下图(a)中所描述,在模式 P = ababaca 和文本 T 的匹配过程中,模板的一个特定位移 s,q = 5 个字符已经匹配成功,但模式 P 的第 6 个字符不能与相应的文本字符匹配。
|
||||
* 我们来观察一下朴素的字符串匹配算法的操作过程。如下图(a)中所描述,在模式 P = ababaca 和文本 T 的匹配过程中,模板的一个特定位移 s,q = 5 个字符已经匹配成功,但模式 P 的第 6 个字符不能与相应的文本字符匹配。
|
||||

|
||||
|
||||
此时,q 个字符已经匹配成功的信息确定了相应的文本字符,而知道这 q 个文本字符,就使我们能够立即确定某些位移是非法的。例如上图(a)中,我们可以判断位移 s+1 是非法的,因为模式 P 的第一个字符 a 将与模式的第二个字符 b 匹配的文本字符进行匹配,显然是不匹配的。而图(b)中则显示了位移 s’ = s+2 处,使模式 P 的前三个字符和相应的三个文本字符对齐后必定会匹配。KMP 算法的基本思路就是设法利用这些已知信息,不要把 "搜索位置" 移回已经比较过的位置,而是继续把它向后面移,这样就提高了匹配效率。
|
||||
* 此时,q 个字符已经匹配成功的信息确定了相应的文本字符,而知道这 q 个文本字符,就使我们能够立即确定某些位移是非法的。例如上图(a)中,我们可以判断位移 s+1 是非法的,因为模式 P 的第一个字符 a 将与模式的第二个字符 b 匹配的文本字符进行匹配,显然是不匹配的。而图(b)中则显示了位移 s’ = s+2 处,使模式 P 的前三个字符和相应的三个文本字符对齐后必定会匹配。KMP 算法的基本思路就是设法利用这些已知信息,不要把 "搜索位置" 移回已经比较过的位置,而是继续把它向后面移,这样就提高了匹配效率。
|
||||
|
||||
### 算法原理
|
||||
> The basic idea behind KMP’s algorithm is: whenever we detect a mismatch (after some matches), we already know some of the characters in the text (since they matched the pattern characters prior to the mismatch). We take advantage of this information to avoid matching the characters that we know will anyway match.
|
||||
|
||||
|
||||
已知模式 P[1..q] 与文本 T[s+1..s+q] 匹配,那么满足 P[1..k] = T[s’+1..s’+k] 其中 s’+k = s+q 的最小位移 s’ > s 是多少?这样的位移 s’ 是大于 s 的但未必非法的第一个位移,因为已知 T[s+1..s+q] 。在最好的情况下有 s’ = s+q,因此立刻能排除掉位移 s+1, s+2 .. s+q-1。在任何情况下,对于新的位移 s’,无需把 P 的前 k 个字符与 T 中相应的字符进行比较,因为它们肯定匹配。
|
||||
* 已知模式 P[1..q] 与文本 T[s+1..s+q] 匹配,那么满足 P[1..k] = T[s’+1..s’+k] 其中 s’+k = s+q 的最小位移 s’ > s 是多少?这样的位移 s’ 是大于 s 的但未必非法的第一个位移,因为已知 T[s+1..s+q] 。在最好的情况下有 s’ = s+q,因此立刻能排除掉位移 s+1, s+2 .. s+q-1。在任何情况下,对于新的位移 s’,无需把 P 的前 k 个字符与 T 中相应的字符进行比较,因为它们肯定匹配。
|
||||
|
||||
可以用模式 P 与其自身进行比较,以预先计算出这些必要的信息。例如上图(c)中所示,由于 T[s’+1..s’+k] 是文本中已经知道的部分,所以它是字符串 Pq 的一个后缀。
|
||||
* 可以用模式 P 与其自身进行比较,以预先计算出这些必要的信息。例如上图(c)中所示,由于 T[s’+1..s’+k] 是文本中已经知道的部分,所以它是字符串 Pq 的一个后缀。
|
||||
|
||||
此处我们引入模式的前缀函数 π(Pai),π 包含有模式与其自身的位移进行匹配的信息。这些信息可用于避免在朴素的字符串匹配算法中,对无用位移进行测试。
|
||||
* 此处我们引入模式的前缀函数 π(Pai),π 包含有模式与其自身的位移进行匹配的信息。这些信息可用于避免在朴素的字符串匹配算法中,对无用位移进行测试。
|
||||
$$
|
||||
π[q] = max {k : k < q and Pk ⊐ Pq}
|
||||
$$
|
||||
|
||||
π[q] 代表当前字符之前的字符串中,最长的共同前缀后缀的长度。(π[q] is the length of the longest prefix of P that is a proper suffix of Pq.)
|
||||
* π[q] 代表当前字符之前的字符串中,最长的共同前缀后缀的长度。(π[q] is the length of the longest prefix of P that is a proper suffix of Pq.)
|
||||
下图给出了关于模式 P = ababababca 的完整前缀函数 π,可称为部分匹配表(Partial Match Table)。
|
||||
|
||||
### 计算过程:
|
||||
@@ -237,7 +237,7 @@ $$
|
||||
* π[10] = 1,ababababca,前缀 .. a ..,后缀 .. a ..,共有元素最大长度为 1;
|
||||
|
||||
### 算法原理
|
||||
KMP 算法 KMP-MATCHER 中通过调用 COMPUTE-PREFIX-FUNCTION 函数来计算部分匹配表。
|
||||
* KMP 算法 KMP-MATCHER 中通过调用 COMPUTE-PREFIX-FUNCTION 函数来计算部分匹配表。
|
||||
```
|
||||
1 KMP-MATCHER(T, P)
|
||||
2 n ← length[T]
|
||||
@@ -267,17 +267,17 @@ KMP 算法 KMP-MATCHER 中通过调用 COMPUTE-PREFIX-FUNCTION 函数来计算
|
||||
11 return π
|
||||
```
|
||||
|
||||
预处理过程 COMPUTE-PREFIX-FUNCTION 的运行时间为 Θ(m),KMP-MATCHER 的匹配时间为 Θ(n)。
|
||||
* 预处理过程 COMPUTE-PREFIX-FUNCTION 的运行时间为 Θ(m),KMP-MATCHER 的匹配时间为 Θ(n)。
|
||||
|
||||
相比较于 NAIVE-STRING-MATCHER,KMP-MATCHER 的主要优化点就是在当确定字符不匹配时对于 pattern 的位移。
|
||||
* 相比较于 NAIVE-STRING-MATCHER,KMP-MATCHER 的主要优化点就是在当确定字符不匹配时对于 pattern 的位移。
|
||||
|
||||
NAIVE-STRING-MATCHER 的位移效果是:文本向后移一位,模式从头开始。
|
||||
* NAIVE-STRING-MATCHER 的位移效果是:文本向后移一位,模式从头开始。
|
||||
```
|
||||
s = s - j + 1;
|
||||
j = 0;
|
||||
```
|
||||
|
||||
KMP-MATCHER 首先对模式做了获取共同前缀后缀最大长度的预处理操作,位移过程是先将模式向后移 partial_match_length - table[partial_match_length - 1],然后再判断是否匹配。这样通过对已匹配字符串的已知信息的利用,可以有效节省比较数量。
|
||||
* KMP-MATCHER 首先对模式做了获取共同前缀后缀最大长度的预处理操作,位移过程是先将模式向后移 partial_match_length - table[partial_match_length - 1],然后再判断是否匹配。这样通过对已匹配字符串的已知信息的利用,可以有效节省比较数量。
|
||||
```
|
||||
if (j != 0)
|
||||
j = lps[j - 1];
|
||||
@@ -285,7 +285,7 @@ KMP-MATCHER 首先对模式做了获取共同前缀后缀最大长度的预处
|
||||
s++;
|
||||
```
|
||||
|
||||
下面描述了当发现字符 j 与 c 不匹配时的位移效果。
|
||||
* 下面描述了当发现字符 j 与 c 不匹配时的位移效果。
|
||||
```
|
||||
// partial_match_length - table[partial_match_length - 1]
|
||||
rrababababjjjjjiiooorababababcauuu
|
||||
@@ -308,16 +308,16 @@ KMP-MATCHER 首先对模式做了获取共同前缀后缀最大长度的预处
|
||||
xx-
|
||||
ababababca
|
||||
```
|
||||
综上可知,KMP 算法的主要特点是:
|
||||
1. 需要对模式字符串做预处理;
|
||||
2. 预处理阶段需要额外的 O(m) 空间和复杂度;
|
||||
3. 匹配阶段与字符集的大小无关;
|
||||
4. 匹配阶段至多执行 2n - 1 次字符比较;
|
||||
5. 对模式中字符的比较顺序时从左到右;
|
||||
* 综上可知,KMP 算法的主要特点是:
|
||||
1. 需要对模式字符串做预处理;
|
||||
2. 预处理阶段需要额外的 O(m) 空间和复杂度;
|
||||
3. 匹配阶段与字符集的大小无关;
|
||||
4. 匹配阶段至多执行 2n - 1 次字符比较;
|
||||
5. 对模式中字符的比较顺序时从左到右;
|
||||
|
||||
### 算法实现
|
||||
下面是 KMP-MATCHER 的代码示例。
|
||||
```
|
||||
* 下面是 KMP-MATCHER 的代码示例。
|
||||
```java
|
||||
1 namespace StringMatching
|
||||
2 {
|
||||
3 class Program
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
> 主要是利用位运算解决一些巧妙的问题。
|
||||
& ^ ~ |
|
||||
|
||||
* n & (n - 1) 会把n中的最后一个1变成0
|
||||
* 相同的数 ^抑或运算等于零。不同的数^抑或运算等于1
|
||||
## 1 基础
|
||||
|
||||
向下整除 n // 2n//2 等价于 右移一位 n >> 1n>>1 ;
|
||||
取余数 n \% 2n%2 等价于 判断二进制最右一位值 n \& 1n&1
|
||||
### 位运算
|
||||
|
||||
符号|说明
|
||||
|----|----|
|
||||
& | 按位与
|
||||
^ | 按位异或
|
||||
~ | 按位取反
|
||||
| | 按位或
|
||||
\>\> | 右移
|
||||
<< | 左移
|
||||
|
||||
## 2 特殊性质
|
||||
操作 | 性质
|
||||
|-----| -----|
|
||||
n & (n - 1) | n中的最后一个1变成0
|
||||
^抑或运算 | 相同的数抑或运算等于零。不同的数抑或运算等于1
|
||||
n/2 | 等价于 右移一位 n >> 1
|
||||
n*2 | 等价于 左移一位 n << 1
|
||||
n % 2 |等价于 判断二进制最右一位值 n \& 1
|
||||
|
||||
|
||||
### 屏蔽计算,作为递归的终止条件
|
||||
## 3 常见算法
|
||||
|
||||
### 快速幂
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
|
||||
* 求解问题算法的复杂性一般都与问题规模相关,问题规模越小越容易处理。
|
||||
* 分治法的基本思想是,将一个难以直接解决的大问题,分解为**规模较小**的**相同类型**的子问题,直至这些子问题容易直接求解,并且可以利用这些子问题的解求出原问题的解。各个击破,分而治之。
|
||||
* 分治法产生的子问题一般是原问题的较小模式,这就为使用递归技术提供了方便。递归是分治法中最常用的技术。
|
||||
* 分治法产生的子问题一般是原问题的较小模式,这就为使用递归技术提供了方便。**递归是分治法中最常用的技术**。
|
||||
|
||||

|
||||
|
||||
|
||||
### 分治法解决问题的先决条件
|
||||
* 该问题的规模缩小到一定的程度就可以容易地解决;
|
||||
* 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
|
||||
* 利用该问题分解出的子问题的解可以合并为该问题的解;
|
||||
* 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
|
||||
* 该问题的**规模缩小到一定的程度就可以容易地解决**;
|
||||
* 该问题可以分解为若干个规模较小的相同问题,即该问题具有**最优子结构性质;**
|
||||
* 利用该问题分解出的**子问题的解可以合并为该问题的解**;
|
||||
* 该问题所分解出的**各个子问题是相互独立**的,即子问题之间不包含公共的子问题。
|
||||
|
||||
### 分治法的步骤
|
||||
一般来说,分治法的求解过程由以下三个阶段组成:
|
||||
@@ -44,7 +44,7 @@ divide-and-conquer(P){
|
||||
## 0 递归法概述
|
||||
|
||||
### 基本思想
|
||||
直接或间接的调用自身的算法称为递归算法。用函数自身给出定义的函数称为递归函数。
|
||||
* 直接或间接的调用自身的算法称为递归算法。用函数自身给出定义的函数称为递归函数。
|
||||
|
||||
### 线性收缩递归算法
|
||||
* 递推关系式
|
||||
@@ -81,7 +81,7 @@ $$
|
||||
$$
|
||||
T(n)=O(n^{\log_ba})+O(f(n))\log_bn
|
||||
$$
|
||||
其真正的时间复杂度,由前后两部分决定。可以通过计算,得到较大部分的时间复杂度,为整体的时间复杂度。
|
||||
* 其真正的时间复杂度,由前后两部分决定。可以通过计算,得到较大部分的时间复杂度,为整体的时间复杂度。
|
||||
|
||||
* 当f(n)为常数时
|
||||
$$
|
||||
@@ -100,12 +100,42 @@ T(n)=\begin{cases}
|
||||
O(n^{log_b^a})&a>b
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
### 直接递归和简介递归
|
||||
* 直接递归,是指函数自己调用自己的情况。
|
||||
* 间接递归,是指调用其他函数时,在其他函数中又调用了自己的情况。
|
||||
### 头递归和尾递归
|
||||
|
||||
* 头递归:递归发生在函数的其他处理代码之前(或理解为,递归发生在函数的头部或顶部)
|
||||
* 需要使用后续递归后的记过,参与本层的计算,然后返回给上一层。这时,需要在下层的递归完成后,才能计算。如归并计算,需要下一层拍好顺序,这一次才会执行归并操作。
|
||||
* 尾递归:递归发生在函数其他处理代码的后面(或理解为,递归发生在函数的尾部)
|
||||
* 本层内的计算是为了确定递归过程中的条件。且下一层返回的结果,能够作为本层的结果返回。本层不知道改成的结果,结果取决于最底层。如搜索树的最大深度,只有递归到叶节点才知道最大深度是多少。
|
||||
* 中间部分递归:递归发生在函数体的中间部分。
|
||||
* 头递归和尾递归只是描述递归发生的相对顺序。实际上在递归过程中,递归可以发生在任何位置。递归前的代码负责处理本层的与下一层无关的内容。递归后的代码负责利用下一层的返回值或其他变量处理内容。
|
||||
|
||||
### 递归的终止条件
|
||||
* 终止递归:终止递归有两种思路。
|
||||
1. 本层不满足条件。终止递归。当进入本层后,首先判断递归是否终止。
|
||||
2. 下一层不满足条件。终止递归。当要进行递归前,判断是否还需要进行递归。前提是直到下一层是否满足条件的计算。
|
||||
* 本层判断更符合递归的思想,且在实现过程中,一般也相对简单。
|
||||
|
||||
### 递归的实现
|
||||
|
||||
* 设计递归的终止条件
|
||||
* 设计递归的返回值
|
||||
* 递归前的数据处理
|
||||
* 递归后的数据处理
|
||||
|
||||
|
||||
|
||||
### 递归算法理解
|
||||
|
||||
* 递归算法本质上是一种自顶向下的思考模式。即数学上所说的归纳法。有结果一步一步归纳,得到验证其条件的正确性。问题规模逐渐变小。
|
||||
* 非递归算法的本质是一种自底向上的思考模式。即数学上所说的推导法。将一些零碎的部分逐渐求解,渐渐得到最终的结果。问题逐渐组装成目标问题。
|
||||
|
||||
在使用递归的时候,应该从顶层考虑怎么分割。在使用非递归算法的时候,应该考虑怎样从顶层进行组合。
|
||||
* 在使用递归的时候,应该从顶层考虑怎么分割。在使用非递归算法的时候,应该考虑怎样从顶层进行组合。
|
||||
|
||||
###
|
||||
|
||||
## 0 减治法概述
|
||||
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
## 1 概述
|
||||
### 基本思想
|
||||
动态规划算法与分治法类似,其思想把求解的问题分成许多阶段或多个子问题,然后按顺序求解各子问题。最后一个阶段或子问题的解就是初始问题的解。
|
||||
* 动态规划算法与分治法类似,其思想把求解的问题分成许多阶段或多个子问题,然后按顺序求解各子问题。最后一个阶段或子问题的解就是初始问题的解。
|
||||
|
||||
* 动态规划基本思想是保留已解决的子问题的解,在需要时再查找已求得的解,就可以避免大量重复计算,进而提升算法效率。
|
||||
|
||||
动态规划基本思想是保留已解决的子问题的解,在需要时再查找已求得的解,就可以避免大量重复计算,进而提升算法效率。
|
||||
要素
|
||||
|
||||
### 对比分治法
|
||||
动态规划中分解得到的子问题不是互相独立的。不同子问题的数目常常只有多项式级,用分治法求解时,有些子问题被重复计算了多次,从而导致分治法求解问题时间复杂度极高。
|
||||
* 动态规划中分解得到的**子问题不是互相独立的**。不同子问题的数目常常只有多项式级,用分治法求解时,有些子问题被重复计算了多次,从而导致分治法求解问题时间复杂度极高。
|
||||
|
||||
动态规划的基本思想是保留已经解决的子问题的解。在需要的时候查找已知的解。避免大量重复的计算而提高效率。
|
||||
* 动态规划的基本思想是保留已经解决的子问题的解。在需要的时候查找已知的解。避免大量重复的计算而提高效率。
|
||||
|
||||
### 条件
|
||||
1. 最优子结构/最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
|
||||
2. 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
|
||||
3. 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
|
||||
1. **最优子结构/最优化原理**:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
|
||||
2. **无后效性**:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
|
||||
3. **有重叠子问题**:即子问题之间是**不独立**的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
|
||||
|
||||
### 基本步骤
|
||||
1. 找出最优解的性质,刻画其结构特征。
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
## 概述
|
||||
|
||||
### 思想
|
||||
贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。
|
||||
贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。
|
||||
* 贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。
|
||||
* 贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。
|
||||
### 特点
|
||||
* 贪心算法不能对所有问题都得到整体最优解。但对许多问题他能产生整体最优解,或者最优解的近似。
|
||||
* 贪心算法中较大子问题的解敲好包含了较小子问题的解作为子集。与动态规划算法中的优化原则本质上一致。
|
||||
* 动态规划算法在某一步决定优化函数的最大或最小值时,需要考虑他的所有子问题的优化函数的值。然后从中选出最有的结果。贪心算法的每步判断时,不考虑子问题的计算结果,而是根据当前的情况采取“只顾眼前”的贪心策略决定取舍。
|
||||
|
||||
### 贪心算法的适用条件
|
||||
* 最优子结构性质
|
||||
* **最优子结构性质**
|
||||
一个问题的最优解包含其子问题的最优解。问题具有最优子结构性质时,可以用动态规划算法或者贪心算法求解。
|
||||
* 贪心选择性质
|
||||
* **贪心选择性质**
|
||||
* 所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
|
||||
* 动态规划算法通常是自底向上的方式来求解各个子问题。贪心算法同行一自顶向下的方式,一迭代的方式作出相机的谈心选择。每左慈谈心选择就将所求问题简化为规模更小的子问题。
|
||||
* 对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所做的贪心选择最终导致问题的整体最优解。
|
||||
|
||||
@@ -3,29 +3,28 @@
|
||||
## Huffman算法
|
||||
|
||||
### 问题描述
|
||||
一个字符串文件,我们希望尽可能多地压缩文件,但源文件能够很容易地被重建。
|
||||
* 一个字符串文件,我们希望尽可能多地压缩文件,但源文件能够很容易地被重建。
|
||||
|
||||
用特定的比特串表示每个字符,称为字符的编码,文件的大小取决于文件中的字符数n。我们可以使用一种定长编码,对每个字符赋予一个长度同为m (m≥log2n)的比特串。
|
||||
* 用特定的比特串表示每个字符,称为字符的编码,文件的大小取决于文件中的字符数n。我们可以使用一种定长编码,对每个字符赋予一个长度同为m (m≥log2n)的比特串。
|
||||
|
||||
设文件中的字符集是C={c1,c2,…, cn}, 又设f(ci), 1≤i≤n,是文件中字符ci的频度,即文件中ci出现的次数。
|
||||
* 设文件中的字符集是C={c1,c2,…, cn}, 又设f(ci), 1≤i≤n,是文件中字符ci的频度,即文件中ci出现的次数。
|
||||
|
||||
由于有些字符的频度可能远大于另外一些字符的频度,所以用变长的编码。
|
||||
* 由于有些字符的频度可能远大于另外一些字符的频度,所以用变长的编码。
|
||||
|
||||
去除编码的二义性:
|
||||
* 去除编码的二义性:
|
||||
|
||||
当编码在长度上变化时,我们规定一个字符的编码不能是另一个字符编码的前缀(即词头),这种码称为前缀码。
|
||||
* 当编码在长度上变化时,我们规定一个字符的编码不能是另一个字符编码的前缀(即词头),这种码称为前缀码。
|
||||
|
||||
例如,如果我们把编码10和101赋予字符“a” 和“b”就会存在二义性,不清楚10究竟是“a” 的编码还是字符“b”的编码的前缀。
|
||||
* 例如,如果我们把编码10和101赋予字符“a” 和“b”就会存在二义性,不清楚10究竟是“a” 的编码还是字符“b”的编码的前缀。
|
||||
|
||||
一旦满足前缀约束,编码即不具备二义性,可以扫描比特序列直到找到某个字符的编码。
|
||||
* 一旦满足前缀约束,编码即不具备二义性,可以扫描比特序列直到找到某个字符的编码。
|
||||
|
||||
|
||||
### 算法原理
|
||||
|
||||
由Huffman算法构造的编码满足前缀约束,并且最小化压缩文件的大小。
|
||||
算法重复下面的过程直到C仅由一个字符组成:
|
||||
* 设ci和cj是两个有最小频度的字符,建立一个新节点c,它的频度是ci和cj频度的和,使ci和cj为c的子节点,
|
||||
* 令C = (C-{ci, cj})∪{c}。
|
||||
* 由Huffman算法构造的编码满足前缀约束,并且最小化压缩文件的大小。算法重复下面的过程直到C仅由一个字符组成:
|
||||
* 设ci和cj是两个有最小频度的字符,建立一个新节点c,它的频度是ci和cj频度的和,使ci和cj为c的子节点,
|
||||
* 令C = (C-{ci, cj})∪{c}。
|
||||
|
||||
### 算法过程
|
||||
|
||||
@@ -37,7 +36,7 @@
|
||||
3. 构建树后然后进行编码,从根节点,每个路径上左0右1
|
||||
### 算法效率
|
||||
|
||||
O(n\log n)
|
||||
$O(n/log n)$
|
||||
|
||||
### 算法实现
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
## 0 概述
|
||||
|
||||
### 基本思想
|
||||
回溯法是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。回溯法的基本思想:该方法系统地搜索一个问题的所有的解或任一解。
|
||||
* 回溯法是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。回溯法的基本思想:该方法系统地搜索**一个问题的所有的解或任一解**。
|
||||
|
||||
与剪枝相结合,也称为**回溯-剪枝法**。
|
||||
* 与剪枝相结合,也称为**回溯-剪枝法**。
|
||||
|
||||
### 要素
|
||||
* **解向量**-问题解的表示:
|
||||
@@ -17,32 +17,33 @@
|
||||
* **解空间**:
|
||||
对于一个实例,解向量满足显示约束条件的所有多元组,构成了该实例的一个解空间。
|
||||
|
||||
### 基本方法:
|
||||
### 基本方法
|
||||
|
||||
明确定义问题的解空间,将问题的解空间组织成树的结构,通过采用系统的方法隐含搜索解空间树,从而得到问题解。回溯法的基本做法是搜索,是一种组织的井井有条的,能避免不必要搜索的穷举式搜索。搜索策略主要有:深度优先、广度优先、函数优先、广度深度结合。
|
||||
|
||||
节点分支判定条件:
|
||||
**节点分支判定条件:**
|
||||
* 满足约束条件:分支扩展解向量。
|
||||
* 不满足约束条件:回溯到当前节点的父结点。
|
||||
|
||||
|
||||
结点状态:
|
||||
**结点状态:**
|
||||
* 白结点:尚未访问的节点
|
||||
* 灰节点:正在访问以该节点为跟的子树。
|
||||
* 黑节点:以该节点为根的子树遍历完成。
|
||||
|
||||
存储当前索索路径
|
||||
**存储当前索索路径**
|
||||
|
||||
搜索策略(避免无效搜索)
|
||||
**搜索策略(避免无效搜索)**
|
||||
* **约束函数**:在扩展节点处减去不满足约束条件的子树。
|
||||
* **界限函数**:在扩展节点处减去得不到最优解的子树。
|
||||
|
||||
### 回溯法的适用条件
|
||||
适用于搜索问题和优化问题
|
||||
* 适用于搜索问题和优化问题
|
||||
|
||||
必要条件
|
||||
* **多米诺性质**:叶子节点的解一定满足其父节点。叶子结点为真则父节点一定为真。同理父节点为假则叶子结点一定为假(逆否命题)。用父节点为假的情况进行剪枝操作。
|
||||
|
||||
设P(x1,x2,…,xi)是关于向量<x1,x2,…,xi>的某个性质,那么P(x1,x2,…,xi+1)真蕴含P(x1,x2,…,xi) 为真,即
|
||||
* 设P(x1,x2,…,xi)是关于向量<x1,x2,…,xi>的某个性质,那么P(x1,x2,…,xi+1)真蕴含P(x1,x2,…,xi) 为真,即
|
||||
P(x1,x2,…,xi+1) → P(x1,x2,…,xi) (0<i<n) (n为向量维数)
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
### 基本思想
|
||||
|
||||
在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。
|
||||
* 在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。
|
||||
|
||||
此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。
|
||||
* 此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。
|
||||
|
||||
### 常见的两种分支界限法
|
||||
* 队列式(FIFO)分支限界法:按照队列先进先出(FIFO)原则选取下一个节点为扩展节点。
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
### 使用条件
|
||||
|
||||
* 问题的多米诺性质
|
||||
* **问题的多米诺性质**叶子节点的解一定满足其父节点。叶子结点为真则父节点一定为真。同理父节点为假则叶子结点一定为假(逆否命题)。用父节点为假的情况进行剪枝操作。
|
||||
|
||||
* 求解最优解或一个可行解
|
||||
|
||||
@@ -29,4 +29,5 @@
|
||||
5. 分支限界发的核心思想在于**界的设计**
|
||||
|
||||
### 分支限界法的程序结构
|
||||
迭代方法
|
||||
|
||||
* 队列+循环的方法
|
||||
@@ -3,13 +3,11 @@
|
||||
|
||||
### 随机算法的概述
|
||||
|
||||
将算法必须对所有可能的输入都正确地求解问题的条件放宽,只要求它的可能不正确性解能够相对安全地忽略掉,比如说它的出现可能性非常低;
|
||||
而且也不要求对于特定的输入,算法的每一次运行的输出都必须相同。
|
||||
* 将算法必须对所有可能的输入都正确地求解问题的条件放宽,只要求它的可能不正确性解能够相对安全地忽略掉,比如说它的出现可能性非常低;而且也不要求对于特定的输入,算法的每一次运行的输出都必须相同。
|
||||
|
||||
随机算法可以做如下定义:
|
||||
它是在接收输入的同时,为了随机选择的目的,还接收一串随机比特流并且在运行过程中使用该比特流的算法。
|
||||
* 随机算法可以做如下定义:它是在接收输入的同时,为了随机选择的目的,还接收一串随机比特流并且在运行过程中使用该比特流的算法。
|
||||
|
||||
一个随机算法在不同的运行中对于相同的输入可以有不同的结果。由此得出对于相同的输入两次不同的随机算法的执行时间可能不同。
|
||||
* 一个随机算法在不同的运行中对于相同的输入可以有不同的结果。由此得出对于相同的输入两次不同的随机算法的执行时间可能不同。
|
||||
|
||||
### 随机算法的优点
|
||||
|
||||
@@ -39,9 +37,7 @@ $$
|
||||
|
||||
|
||||
## 4 蒙特卡洛算法
|
||||
蒙特·卡罗方法(Monte Carlo method),也称统计模拟方法,是二十世纪四十年代中期由于科学技术的发展和电子计算机的发明,而被提出的一种以概率统计理论为指导的一类非常重要的数值计算方法。是指使用随机数(或更常见的伪随机数)来解决很多计算问题的方法。与它对应的是确定性算法。蒙特·卡罗方法在金融工程学,宏观经济学,计算物理学(如粒子输运计算、量子热力学计算、空气动力学计算)等领域应用广泛。
|
||||
|
||||
|
||||
* 蒙特·卡罗方法(Monte Carlo method),也称统计模拟方法,是二十世纪四十年代中期由于科学技术的发展和电子计算机的发明,而被提出的一种以概率统计理论为指导的一类非常重要的数值计算方法。是指使用随机数(或更常见的伪随机数)来解决很多计算问题的方法。与它对应的是确定性算法。蒙特·卡罗方法在金融工程学,宏观经济学,计算物理学(如粒子输运计算、量子热力学计算、空气动力学计算)等领域应用广泛。
|
||||
|
||||
### 随机投点法计算$\pi$
|
||||
|
||||
@@ -49,35 +45,35 @@ $$
|
||||
|
||||
## 5 拉斯维加斯算法
|
||||
|
||||
拉斯维加斯算法的一个显著特征是它所作的随机性决策有可能导致算法找不到所需的解。因此通常用一个bool型函数表示拉斯维加斯算法。
|
||||
* 拉斯维加斯算法的一个显著特征是它所作的随机性决策有可能导致算法找不到所需的解。因此通常用一个bool型函数表示拉斯维加斯算法。
|
||||
|
||||
### 拉斯维加斯算法+回溯法 解决N皇后问题
|
||||
|
||||
考虑用拉斯维加斯算法解决N皇后问题:
|
||||
* 考虑用拉斯维加斯算法解决N皇后问题:
|
||||
|
||||
对于n后问题的任何一个解而言,每一个皇后在棋盘上的位置无任何规律,不具有系统性,而更象是随机放置的。由此容易想到下面的拉斯维加斯算法。
|
||||
在棋盘上相继的各行中随机地放置皇后,并注意使新放置的皇后与已放置的皇后互不攻击,直至n个皇后已相容地放置好,或已没有下一个皇后的可放置位置时为止。注意这里解决的是找到其中一个方法,求不是求出N皇后的全部解。
|
||||
* 对于n后问题的任何一个解而言,每一个皇后在棋盘上的位置无任何规律,不具有系统性,而更象是随机放置的。由此容易想到下面的拉斯维加斯算法。
|
||||
* 在棋盘上相继的各行中随机地放置皇后,并注意使新放置的皇后与已放置的皇后互不攻击,直至n个皇后已相容地放置好,或已没有下一个皇后的可放置位置时为止。注意这里解决的是找到其中一个方法,求不是求出N皇后的全部解。
|
||||
|
||||
这里提前说明一下,否则不好理解。
|
||||
* 这里提前说明一下,否则不好理解。
|
||||
|
||||
接下来的这个用Las Vegas算法解决N皇后问题,我们采用的是随机放置位置策略和回溯法相结合,具体就是比如八皇后中,前几行选择用随机法放置皇后,剩下的选择用回溯法解决。
|
||||
* 接下来的这个用Las Vegas算法解决N皇后问题,我们采用的是随机放置位置策略和回溯法相结合,具体就是比如八皇后中,前几行选择用随机法放置皇后,剩下的选择用回溯法解决。
|
||||
|
||||
这个程序不是很好理解,有的地方我特别说明了是理解程序的关键,大家看时一定要认真了,另外,王晓东的书上先是用单纯的随机法解决,大家可以先去理解书上这个例子。然后再来分析我这个程序。不过那本书上关于这一块错误比较多,大家看时要注意哪些地方他写错了。
|
||||
* 这个程序不是很好理解,有的地方我特别说明了是理解程序的关键,大家看时一定要认真了,另外,王晓东的书上先是用单纯的随机法解决,大家可以先去理解书上这个例子。然后再来分析我这个程序。不过那本书上关于这一块错误比较多,大家看时要注意哪些地方他写错了。
|
||||
|
||||
|
||||
## 6 蒙特卡洛方法与拉斯维加斯算法对比
|
||||
|
||||
### 定义
|
||||
蒙特卡罗是一类随机方法的统称。这类方法的特点是,可以在随机采样上计算得到近似结果,随着采样的增多,得到的结果是正确结果的概率逐渐加大,但在(放弃随机采样,而采用类似全采样这样的确定性方法)获得真正的结果之前,无法知道目前得到的结果是不是真正的结果。
|
||||
* 蒙特卡罗是一类随机方法的统称。这类方法的特点是,可以在随机采样上计算得到近似结果,随着采样的增多,得到的结果是正确结果的概率逐渐加大,但在(放弃随机采样,而采用类似全采样这样的确定性方法)获得真正的结果之前,无法知道目前得到的结果是不是真正的结果。
|
||||
|
||||
|
||||
拉斯维加斯方法是另一类随机方法的统称。这类方法的特点是,随着采样次数的增多,得到的正确结果的概率逐渐加大,如果随机采样过程中已经找到了正确结果,该方法可以判别并报告,但在放弃随机采样,而采用类似全采样这样的确定性方法之前,不保证能找到任何结果(包括近似结果)
|
||||
* 拉斯维加斯方法是另一类随机方法的统称。这类方法的特点是,随着采样次数的增多,得到的正确结果的概率逐渐加大,如果随机采样过程中已经找到了正确结果,该方法可以判别并报告,但在放弃随机采样,而采用类似全采样这样的确定性方法之前,不保证能找到任何结果(包括近似结果)
|
||||
|
||||
### 场景
|
||||
|
||||
假如筐里有100个苹果,让我每次闭眼拿1个,挑出最大的。于是我随机拿1个,再随机拿1个跟它比,留下大的,再随机拿1个……我每拿一次,留下的苹果都至少不比上次的小。拿的次数越多,挑出的苹果就越大,但我除非拿100次,否则无法肯定挑出了最大的。这个挑苹果的算法,就属于蒙特卡罗算法——尽量找好的,但不保证是最好的。
|
||||
* 假如筐里有100个苹果,让我每次闭眼拿1个,挑出最大的。于是我随机拿1个,再随机拿1个跟它比,留下大的,再随机拿1个……我每拿一次,留下的苹果都至少不比上次的小。拿的次数越多,挑出的苹果就越大,但我除非拿100次,否则无法肯定挑出了最大的。这个挑苹果的算法,就属于蒙特卡罗算法——尽量找好的,但不保证是最好的。
|
||||
|
||||
而拉斯维加斯算法,则是另一种情况。假如有一把锁,给我100把钥匙,只有1把是对的。于是我每次随机拿1把钥匙去试,打不开就再换1把。我试的次数越多,打开(最优解)的机会就越大,但在打开之前,那些错的钥匙都是没有用的。这个试钥匙的算法,就是拉斯维加斯的——尽量找最好的,但不保证能找到。
|
||||
* 而拉斯维加斯算法,则是另一种情况。假如有一把锁,给我100把钥匙,只有1把是对的。于是我每次随机拿1把钥匙去试,打不开就再换1把。我试的次数越多,打开(最优解)的机会就越大,但在打开之前,那些错的钥匙都是没有用的。这个试钥匙的算法,就是拉斯维加斯的——尽量找最好的,但不保证能找到。
|
||||
|
||||
### 结论
|
||||
|
||||
@@ -85,5 +81,5 @@ $$
|
||||
|
||||
* 拉斯维加斯算法:采样越多,越有机会找到最优解;
|
||||
|
||||
这两类随机算法之间的选择,往往受到问题的局限。如果问题要求在有限采样内,必须给出一个解,但不要求是最优解,那就要用蒙特卡罗算法。反之,如果问题要求必须给出最优解,但对采样没有限制,那就要用拉斯维加斯算法。
|
||||
* 这两类随机算法之间的选择,往往受到问题的局限。如果问题要求在有限采样内,必须给出一个解,但不要求是最优解,那就要用蒙特卡罗算法。反之,如果问题要求必须给出最优解,但对采样没有限制,那就要用拉斯维加斯算法。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user