From 9fb8b9602fd9342c909d7fcc5af7d3a4d139d413 Mon Sep 17 00:00:00 2001 From: yinkanglong_lab Date: Sat, 13 Mar 2021 22:17:53 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AE=97=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- C++/标准库/2 容器.md | 23 +- C++/标准库/2.1 顺序容器.md | 30 ++- C++/标准库/2.3 容器适配器.md | 6 +- C++/标准库/2.3.cpp | 3 +- C++/标准库/5 字符串.md | 209 +++++++++++++++++- 算法/A类:基本算法/1 算法概述.md | 11 +- 算法/A类:基本算法/10 近似算法.md | 8 +- 算法/A类:基本算法/10.2 邻域搜索算法.md | 4 +- 算法/A类:基本算法/10.3 禁忌搜索算法.md | 4 +- 算法/A类:基本算法/2 算法效率.md | 2 +- 算法/A类:基本算法/3 蛮力法(枚举法).md | 31 ++- 算法/A类:基本算法/3.1 查找算法.md | 194 ++++++++-------- .../A类:基本算法/3.2 搜索算法-广度优先搜索.md | 34 +-- .../A类:基本算法/3.3 搜索算法-深度优先搜索.md | 29 ++- 算法/A类:基本算法/3.4 排序算法-简单排序.md | 4 +- 算法/A类:基本算法/3.6 线性时间选择算法.md | 4 +- 算法/A类:基本算法/3.7 字符串算法-匹配算法.md | 66 +++--- 算法/A类:基本算法/3.9 位运算算法.md | 27 ++- 算法/A类:基本算法/4 递归与分治法.md | 46 +++- 算法/A类:基本算法/5 动态规划.md | 16 +- 算法/A类:基本算法/6 贪心算法.md | 8 +- 算法/A类:基本算法/6.1 Huffman算法.md | 25 +-- 算法/A类:基本算法/7 回溯法.md | 19 +- 算法/A类:基本算法/8 分支限界.md | 9 +- 算法/A类:基本算法/9 随机化.md | 36 ++- 25 files changed, 552 insertions(+), 296 deletions(-) diff --git a/C++/标准库/2 容器.md b/C++/标准库/2 容器.md index 049070c1..63ae1467 100644 --- a/C++/标准库/2 容器.md +++ b/C++/标准库/2 容器.md @@ -26,14 +26,15 @@ ![](2021-03-05-20-17-45.png) ![](2021-03-05-20-17-55.png) -主要包括五类 -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) -### 容器的定义和初始化 +### 容器的构造函数和初始化 ![](2021-03-05-20-26-48.png) @@ -42,13 +43,17 @@ ![](2021-03-05-20-29-29.png) -### 容器大小的操作 +### 容器大小 * size():返回容器中元素的数目 * empty():当size为0是返回true * maxsize():返回容器所能容纳的最大元素数的值。 - +### 插入删除 +* insert()插入对象 +* emplace()元素初始化插入 +* erase()删除指定元素 +* clear()清空 ### 关系运算符 * 容器支持相等和不等的运算。== != diff --git a/C++/标准库/2.1 顺序容器.md b/C++/标准库/2.1 顺序容器.md index d7019151..c977d82a 100644 --- a/C++/标准库/2.1 顺序容器.md +++ b/C++/标准库/2.1 顺序容器.md @@ -3,41 +3,45 @@ > 目录 > * array > * vector -> * deque +> * deque双端队列 > * list > * forward_list > * string//专门用于字符串访问的容器 - +> * vector/deque/list拥有容器所有的操作。首尾相关的操作。 ## 0 顺序容器的基础操作 ### 向顺序容器中添加元素 ![](2021-03-05-20-37-12.png) -* 在尾部添加元素push_back() -* 在头部添加元素push_front() -* 在中间添加元素insert() +* 在尾部添加元素**push_back(),emplace_back()** +* 在头部添加元素**push_front(),emplace_front()** +* 在中间添加元素insert(),emplace() ### 在顺序容器中访问元素 ![](2021-03-05-20-40-51.png) -* 也可以使用迭代器访问元素。 -* at会进行安全检查抛出异常。[]不会进行检查。 +* 也可以使用**迭代器**访问元素。 +* **at**会进行安全检查抛出异常。 +* **[]下标运算符**不会进行检查。 +* **back(),front()** ### 在顺序容器中删除元素 ![](2021-03-05-20-42-30.png) +* 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的特殊操作 -![](2021-03-05-20-54-47.png) ### 改变容器的大小 @@ -49,7 +53,7 @@ > 数组不能copy赋值,但是array可以copy赋值。 ### 定义 - +是静态的连续数组,只有默认初始化。 ``` array arr = {1, 2, 3, 4, 5}; ``` @@ -59,5 +63,7 @@ array arr = {1, 2, 3, 4, 5}; ## 3 deque ## 4 foward_list +### foward_list的特殊操作 +![](2021-03-05-20-54-47.png) ## 5 list diff --git a/C++/标准库/2.3 容器适配器.md b/C++/标准库/2.3 容器适配器.md index f328bebd..b6b7a544 100644 --- a/C++/标准库/2.3 容器适配器.md +++ b/C++/标准库/2.3 容器适配器.md @@ -15,10 +15,11 @@ ### 容器适配器的操作 ![](2021-03-05-21-29-55.png) +* 可以用顺序容器初始化适配器。使用的是顺序容器的拷贝。 ## 1 stack - +* 默认基于deque实现,也可以基于list/vector ### 概念 ### 特有操作 @@ -29,7 +30,8 @@ ## 2 queue和priority_queue ### 概念 - +* queue是基于deque实现的,也可以用vector或list +* priority_queue是基于vector实现的。也可以用deque实现 ### 特有操作 diff --git a/C++/标准库/2.3.cpp b/C++/标准库/2.3.cpp index 23025748..81e84b97 100644 --- a/C++/标准库/2.3.cpp +++ b/C++/标准库/2.3.cpp @@ -8,6 +8,7 @@ int main(){ //stack test deque deq{2,3,4,5}; + stack stk{deq}; int m = stk.top(); stk.pop(); @@ -17,6 +18,6 @@ int main(){ cout< vec; return 0; } \ No newline at end of file diff --git a/C++/标准库/5 字符串.md b/C++/标准库/5 字符串.md index 96c23d79..b823f41f 100644 --- a/C++/标准库/5 字符串.md +++ b/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 +#include +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 +#include +#include +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 < +#include +#include +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的正则匹配搜索算法。 diff --git a/算法/A类:基本算法/1 算法概述.md b/算法/A类:基本算法/1 算法概述.md index 6e3e6b13..a1588e6b 100644 --- a/算法/A类:基本算法/1 算法概述.md +++ b/算法/A类:基本算法/1 算法概述.md @@ -3,7 +3,8 @@ ## 1 基本概念 ### 定义 -算法是一系列解决问题的清晰指令,对于符合一定规范的输入,算法能够在有限时间内获得所要求的输出。算法是解决问题的一种方法或过程,它是由若干条指令组成的有穷序列。 +* 算法是一系列解决问题的清晰指令,对于符合一定规范的输入,算法能够在有限时间内获得所要求的输出。算法是解决问题的一种方法或过程,它是由若干条指令组成的有穷序列。 +* 算法本质上不是数学,而是逻辑。 ### 特征 * 输入:有零或多个外部量作为算法的输入。 @@ -26,18 +27,18 @@ 1. **理解问题**(确定**问题抽象**(将应用问题抽象为数学问题)、**问题分类**(搜索、排序。)) 2. **选择策略**(选择合适的**数据结构**、选择合适的**算法思想**) -3. **算法设计**(设计**算法流程**,伪代码) +3. **算法设计**(确定**算法技术**,设计**算法流程**,数学递推关系和伪代码) 4. **正确性证明**(查看伪代码的流程的正确性,**算法特例**) 5. **算法分析**(分析算法**执行效率**) -6. **程序设计**(编程) +6. **程序设计**(**编程**) ### 更详细的说明 > 自己在处理一个题之前到底应该做哪些事情,或者说,按照怎样的流程?是否应该把这些流程写下来?或者按照某种套路来,将会事半功倍。 * 理解问题、问题分析,问题抽象和问题分类。应该将一个问题,归为某一个类别。问题类别,应该属于某个算法思想下的。也就是说,一个问题类别,应该用某种算法思想来解决。但是两者并不是完全重合。 -* 选择策略,主要是选择使用哪种算法思想。当确定了问题的类别之后。就可以确定其算法思想了。或者说,算法四线和问题类别应该是同时确定的。接下来需要做的是,设计一些静态的东西。例如数据结构等。 -* 算法设计,在静态结构上添加流程。 +* 选择策略,主要是选择使用哪种算法思想(包括蛮力法、递归与分治、动态规划、贪心、回溯、分支限界思想)。当确定了问题的类别之后。就可以确定其算法思想了。或者说,算法思想和问题类别应该是同时确定的。接下来需要做的是,设计一些静态的东西。例如数据结构等。 +* 算法设计,每一类算法思想都有固定的算法技术,比如分治算法思想,对应的递归求解技术。例如回溯法和蛮力法的深度优先搜索思想,使用的是递归的算法技术和栈+循环的算法技术。广度优先搜索或者分治限界的算法思想,对应的是队列+循环的技术。 * 正确性证明,主要分析特例和为考虑到的特殊情况。尽可能举反例,同时完善设计好的算法。 * 算法分析,主要分析算法的执行效率上的可行性。 * 程序设计 diff --git a/算法/A类:基本算法/10 近似算法.md b/算法/A类:基本算法/10 近似算法.md index 3193abbf..9c2bf187 100644 --- a/算法/A类:基本算法/10 近似算法.md +++ b/算法/A类:基本算法/10 近似算法.md @@ -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的近似算法。 diff --git a/算法/A类:基本算法/10.2 邻域搜索算法.md b/算法/A类:基本算法/10.2 邻域搜索算法.md index c64d5f1b..69f4d634 100644 --- a/算法/A类:基本算法/10.2 邻域搜索算法.md +++ b/算法/A类:基本算法/10.2 邻域搜索算法.md @@ -4,6 +4,6 @@ ### 算法原理 -利用邻域结构进行逐步优化的局部搜索算法: +* 利用邻域结构进行逐步优化的局部搜索算法: -算法从一初始可行解 s 出发,利用状态发生器持续地在s 的领域中搜索更好的解,若能找到更优解,则以其替代s 成为新的当前解,然后重复上述过程,直至终止条件满足。 +* 算法从一初始可行解 s 出发,利用状态发生器持续地在s 的领域中搜索更好的解,若能找到更优解,则以其替代s 成为新的当前解,然后重复上述过程,直至终止条件满足。 diff --git a/算法/A类:基本算法/10.3 禁忌搜索算法.md b/算法/A类:基本算法/10.3 禁忌搜索算法.md index 138101b6..5c3542c6 100644 --- a/算法/A类:基本算法/10.3 禁忌搜索算法.md +++ b/算法/A类:基本算法/10.3 禁忌搜索算法.md @@ -5,9 +5,9 @@ ### 算法概述 -禁忌搜索(TS)是对局部邻域搜索的一种扩展,是一种全局优化算法。TS算法通过引入一个禁忌表和相应的禁忌准则来避免局部迂回,并通过“渴望准则”来挽救某些被禁忌的相对优化解,进而保证全局的有效搜索以实现全局优化。 +* 禁忌搜索(TS)是对局部邻域搜索的一种扩展,是一种全局优化算法。TS算法通过引入一个禁忌表和相应的禁忌准则来避免局部迂回,并通过“渴望准则”来挽救某些被禁忌的相对优化解,进而保证全局的有效搜索以实现全局优化。 -标记对应已搜索到的局部最优解的一些对象,并在进一步的迭代搜索中尽量避开这些对象,但不是绝对禁止循环,从而保证对不同的有效搜索途径的探索。 +* 标记对应已搜索到的局部最优解的一些对象,并在进一步的迭代搜索中尽量避开这些对象,但不是绝对禁止循环,从而保证对不同的有效搜索途径的探索。 ### 基本思想 diff --git a/算法/A类:基本算法/2 算法效率.md b/算法/A类:基本算法/2 算法效率.md index 659604ed..f6e26fde 100644 --- a/算法/A类:基本算法/2 算法效率.md +++ b/算法/A类:基本算法/2 算法效率.md @@ -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)\\ diff --git a/算法/A类:基本算法/3 蛮力法(枚举法).md b/算法/A类:基本算法/3 蛮力法(枚举法).md index 594d7611..82fa7ab5 100644 --- a/算法/A类:基本算法/3 蛮力法(枚举法).md +++ b/算法/A类:基本算法/3 蛮力法(枚举法).md @@ -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 -* 构建基本操作的求和表达式: -利用求和公式分析算法的时间复杂度: +* 构建基本操作的求和表达式:利用求和公式分析算法的时间复杂度: ![](image/排序算法.png) ### 程序设计 @@ -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 * 查找算法,一般适用于线性结构。 +> * 搜索算法,一般适用于树和图。 ### 查找定义 -根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。 +* 根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。 ### 查找算法分类: 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. 任意节点的左、右子树也分别为二叉查找树。 -二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的数列。 +* 二叉查找树性质:对二叉查找树进行中序遍历,即可得到有序的数列。 -不同形态的二叉查找树如下图所示: +* 不同形态的二叉查找树如下图所示: ![](image/查找算法-二叉搜索树.jpeg) ### 复杂度分析 -它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。 +* 它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。 -原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。 +* 原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的“93”,我们需要进行n次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。 -下图为二叉树查找和顺序查找以及二分查找性能的对比图: +* 下图为二叉树查找和顺序查找以及二分查找性能的对比图: ![](image/查找算法-二叉树与二分法.png) ## 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还要大。 ![](image/查找算法-2-3树.png) @@ -317,77 +321,73 @@ int main() ![](image/查找算法-2-3树效率.png) ## 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)是黑色(为了简单期间,一般会省略该节点)。 + * 如果一个节点是红色的,则它的子节点必须是黑色的。(也就是说父子节点不能同时为红色) + * 从一个节点到该节点的每一个叶子子孙节点的所有路径上包含相同数目的黑节点。(这一点是平衡的关键) + * 新插入节点默认为红色,插入后需要校验红黑树是否符合规则,不符合则需要进行操作。 ![](image/查找算法-红黑树.png) -红黑树平衡方法 -前面讲到红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色。 - -* 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。 -* 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。 -* 变色:结点的颜色由红变黑或由黑变红。 +* 红黑树平衡方法.前面讲到红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色。 + * 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。 + * 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。 + * 变色:结点的颜色由红变黑或由黑变红。 ### 复杂度分析 -最坏的情况就是,红黑相间的路径长度是全黑路径长度的2倍。 +* 最坏的情况就是,红黑相间的路径长度是全黑路径长度的2倍。 红黑树的平均高度大约为2logn。 -  下图是红黑树在各种情况下的时间复杂度,可以看出红黑树是2-3查找树的一种实现,它能保证最坏情况下仍然具有对数的时间复杂度。 +* 下图是红黑树在各种情况下的时间复杂度,可以看出红黑树是2-3查找树的一种实现,它能保证最坏情况下仍然具有对数的时间复杂度。 + ![](image/查找算法-红黑树效率.png) ## 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树: ![](image/搜索算法-B树.png)    B+树定义: -B+树是对B树的一种变形树,它与B树的差异在于: -* 有k个子结点的结点必然有k个关键码; -* 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。 -* 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。 +* B+树是对B树的一种变形树,它与B树的差异在于: + * 有k个子结点的结点必然有k个关键码; + * 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。 + * 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。 -如下图,是一个B+树: +* 如下图,是一个B+树: ![](image/查找算法-B+树.png) -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算法和其他查找算法的性能对比: ![](image/查找算法-哈希搜索效率.png) diff --git a/算法/A类:基本算法/3.2 搜索算法-广度优先搜索.md b/算法/A类:基本算法/3.2 搜索算法-广度优先搜索.md index 38165e64..59c68869 100644 --- a/算法/A类:基本算法/3.2 搜索算法-广度优先搜索.md +++ b/算法/A类:基本算法/3.2 搜索算法-广度优先搜索.md @@ -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 的其他顶点。 ![](image/广度优先搜索-层次.png) ### 例子 -例如,下面的图中,从顶点 2 开始遍历,当遍历到顶点 0 时,邻接的顶点为 1 和 2,而顶点 2 已经遍历过,如果不做标记,遍历过程将陷入死循环。所以,在 BFS 的算法实现中需要对顶点是否访问过做标记。 +* 例如,下面的图中,从顶点 2 开始遍历,当遍历到顶点 0 时,邻接的顶点为 1 和 2,而顶点 2 已经遍历过,如果不做标记,遍历过程将陷入死循环。所以,在 BFS 的算法实现中需要对顶点是否访问过做标记。 ![](image/广度优先搜索-例子.png) -上图的 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 diff --git a/算法/A类:基本算法/3.3 搜索算法-深度优先搜索.md b/算法/A类:基本算法/3.3 搜索算法-深度优先搜索.md index ddc629af..83762fe1 100644 --- a/算法/A类:基本算法/3.3 搜索算法-深度优先搜索.md +++ b/算法/A类:基本算法/3.3 搜索算法-深度优先搜索.md @@ -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 的算法实现中需要对顶点是否访问过做标记。 ![](image/深度优先搜索-例子.png) -上图的 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; diff --git a/算法/A类:基本算法/3.4 排序算法-简单排序.md b/算法/A类:基本算法/3.4 排序算法-简单排序.md index 920ce6c5..a6f99cc0 100644 --- a/算法/A类:基本算法/3.4 排序算法-简单排序.md +++ b/算法/A类:基本算法/3.4 排序算法-简单排序.md @@ -269,13 +269,13 @@ ### Stable 与 Not Stable 的比较 -稳定排序算法会将相等的元素值维持其相对次序。如果一个排序算法是稳定的,当有两个有相等的元素值 R 和 S,且在原本的列表中 R 出现在 S 之前,那么在排序过的列表中 R 也将会是在 S 之前。 +* 稳定排序算法会将相等的元素值维持其相对次序。如果一个排序算法是稳定的,当有两个有相等的元素值 R 和 S,且在原本的列表中 R 出现在 S 之前,那么在排序过的列表中 R 也将会是在 S 之前。 ![](image/排序算法-稳定性.png) ### O(n2) 与 O(n*logn) 的比较 -合并排序和堆排序在最坏情况下达到上界 O(n*logn),快速排序在平均情况下达到上界 O(n*logn)。对于比较排序算法,我们都能给出 n 个输入的数值,使算法以 Ω(n*logn) 时间运行。 +* 合并排序和堆排序在最坏情况下达到上界 $O(n*logn)$,快速排序在平均情况下达到上界 $O(n*logn)$。对于比较排序算法,我们都能给出 n 个输入的数值,使算法以 $Ω(n*logn)$ 时间运行。 > 注:有关算法复杂度,可参考文章《算法复杂度分析》。有关常用数据结构的复杂度,可参考文章《常用数据结构及复杂度》。 diff --git a/算法/A类:基本算法/3.6 线性时间选择算法.md b/算法/A类:基本算法/3.6 线性时间选择算法.md index 2edb78cc..0a9a6b00 100644 --- a/算法/A类:基本算法/3.6 线性时间选择算法.md +++ b/算法/A类:基本算法/3.6 线性时间选择算法.md @@ -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 的中位数处。本文中所用的中位数总是指下中位数。 * 选择最大值和最小值 * 选择中位数或任意位置值 diff --git a/算法/A类:基本算法/3.7 字符串算法-匹配算法.md b/算法/A类:基本算法/3.7 字符串算法-匹配算法.md index 77bd466a..8cdfc66b 100644 --- a/算法/A类:基本算法/3.7 字符串算法-匹配算法.md +++ b/算法/A类:基本算法/3.7 字符串算法-匹配算法.md @@ -25,20 +25,19 @@ ### 基本步骤和算法效率 -字符串匹配算法通常分为两个步骤:预处理(Preprocessing)和匹配(Matching)。所以算法的总运行时间为预处理和匹配的时间的总和。 - +* 字符串匹配算法通常分为两个步骤:预处理(Preprocessing)和匹配(Matching)。所以算法的总运行时间为预处理和匹配的时间的总和。 ![](image/字符串匹配算法效率.png) -上图描述了常见字符串匹配算法的预处理和匹配时间。 +* 上图描述了常见字符串匹配算法的预处理和匹配时间。 ## 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 @@ ``` ![](image/字符串匹配算法-算法原理.png) -如上图中,对于模式 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 个字符不能与相应的文本字符匹配。 ![](image/字符串匹配算法-KMP.jpg) -此时,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 diff --git a/算法/A类:基本算法/3.9 位运算算法.md b/算法/A类:基本算法/3.9 位运算算法.md index bf3a783c..af88a662 100644 --- a/算法/A类:基本算法/3.9 位运算算法.md +++ b/算法/A类:基本算法/3.9 位运算算法.md @@ -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 常见算法 ### 快速幂 diff --git a/算法/A类:基本算法/4 递归与分治法.md b/算法/A类:基本算法/4 递归与分治法.md index cec85bda..6f0b74e2 100644 --- a/算法/A类:基本算法/4 递归与分治法.md +++ b/算法/A类:基本算法/4 递归与分治法.md @@ -5,16 +5,16 @@ * 求解问题算法的复杂性一般都与问题规模相关,问题规模越小越容易处理。 * 分治法的基本思想是,将一个难以直接解决的大问题,分解为**规模较小**的**相同类型**的子问题,直至这些子问题容易直接求解,并且可以利用这些子问题的解求出原问题的解。各个击破,分而治之。 -* 分治法产生的子问题一般是原问题的较小模式,这就为使用递归技术提供了方便。递归是分治法中最常用的技术。 +* 分治法产生的子问题一般是原问题的较小模式,这就为使用递归技术提供了方便。**递归是分治法中最常用的技术**。 ![](image/分治法原理.png) ### 分治法解决问题的先决条件 -* 该问题的规模缩小到一定的程度就可以容易地解决; -* 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质; -* 利用该问题分解出的子问题的解可以合并为该问题的解; -* 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。 +* 该问题的**规模缩小到一定的程度就可以容易地解决**; +* 该问题可以分解为若干个规模较小的相同问题,即该问题具有**最优子结构性质;** +* 利用该问题分解出的**子问题的解可以合并为该问题的解**; +* 该问题所分解出的**各个子问题是相互独立**的,即子问题之间不包含公共的子问题。 ### 分治法的步骤 一般来说,分治法的求解过程由以下三个阶段组成: @@ -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 减治法概述 diff --git a/算法/A类:基本算法/5 动态规划.md b/算法/A类:基本算法/5 动态规划.md index 6ade6137..77b90c8d 100644 --- a/算法/A类:基本算法/5 动态规划.md +++ b/算法/A类:基本算法/5 动态规划.md @@ -2,20 +2,20 @@ ## 1 概述 ### 基本思想 -动态规划算法与分治法类似,其思想把求解的问题分成许多阶段或多个子问题,然后按顺序求解各子问题。最后一个阶段或子问题的解就是初始问题的解。 +* 动态规划算法与分治法类似,其思想把求解的问题分成许多阶段或多个子问题,然后按顺序求解各子问题。最后一个阶段或子问题的解就是初始问题的解。 + +* 动态规划基本思想是保留已解决的子问题的解,在需要时再查找已求得的解,就可以避免大量重复计算,进而提升算法效率。 -动态规划基本思想是保留已解决的子问题的解,在需要时再查找已求得的解,就可以避免大量重复计算,进而提升算法效率。 -要素 ### 对比分治法 -动态规划中分解得到的子问题不是互相独立的。不同子问题的数目常常只有多项式级,用分治法求解时,有些子问题被重复计算了多次,从而导致分治法求解问题时间复杂度极高。 +* 动态规划中分解得到的**子问题不是互相独立的**。不同子问题的数目常常只有多项式级,用分治法求解时,有些子问题被重复计算了多次,从而导致分治法求解问题时间复杂度极高。 -动态规划的基本思想是保留已经解决的子问题的解。在需要的时候查找已知的解。避免大量重复的计算而提高效率。 +* 动态规划的基本思想是保留已经解决的子问题的解。在需要的时候查找已知的解。避免大量重复的计算而提高效率。 ### 条件 -1. 最优子结构/最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。 -2. 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。 -3. 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势) +1. **最优子结构/最优化原理**:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。 +2. **无后效性**:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。 +3. **有重叠子问题**:即子问题之间是**不独立**的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势) ### 基本步骤 1. 找出最优解的性质,刻画其结构特征。 diff --git a/算法/A类:基本算法/6 贪心算法.md b/算法/A类:基本算法/6 贪心算法.md index ae66f9f7..13080012 100644 --- a/算法/A类:基本算法/6 贪心算法.md +++ b/算法/A类:基本算法/6 贪心算法.md @@ -3,17 +3,17 @@ ## 概述 ### 思想 -贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。 -贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。 +* 贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。 +* 贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。 ### 特点 * 贪心算法不能对所有问题都得到整体最优解。但对许多问题他能产生整体最优解,或者最优解的近似。 * 贪心算法中较大子问题的解敲好包含了较小子问题的解作为子集。与动态规划算法中的优化原则本质上一致。 * 动态规划算法在某一步决定优化函数的最大或最小值时,需要考虑他的所有子问题的优化函数的值。然后从中选出最有的结果。贪心算法的每步判断时,不考虑子问题的计算结果,而是根据当前的情况采取“只顾眼前”的贪心策略决定取舍。 ### 贪心算法的适用条件 -* 最优子结构性质 +* **最优子结构性质** 一个问题的最优解包含其子问题的最优解。问题具有最优子结构性质时,可以用动态规划算法或者贪心算法求解。 -* 贪心选择性质 +* **贪心选择性质** * 所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。 * 动态规划算法通常是自底向上的方式来求解各个子问题。贪心算法同行一自顶向下的方式,一迭代的方式作出相机的谈心选择。每左慈谈心选择就将所求问题简化为规模更小的子问题。 * 对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所做的贪心选择最终导致问题的整体最优解。 diff --git a/算法/A类:基本算法/6.1 Huffman算法.md b/算法/A类:基本算法/6.1 Huffman算法.md index 90a739ab..94240123 100644 --- a/算法/A类:基本算法/6.1 Huffman算法.md +++ b/算法/A类:基本算法/6.1 Huffman算法.md @@ -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)$ ### 算法实现 diff --git a/算法/A类:基本算法/7 回溯法.md b/算法/A类:基本算法/7 回溯法.md index 3db9f78b..3644c00f 100644 --- a/算法/A类:基本算法/7 回溯法.md +++ b/算法/A类:基本算法/7 回溯法.md @@ -3,9 +3,9 @@ ## 0 概述 ### 基本思想 -回溯法是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。回溯法的基本思想:该方法系统地搜索一个问题的所有的解或任一解。 +* 回溯法是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。回溯法的基本思想:该方法系统地搜索**一个问题的所有的解或任一解**。 -与剪枝相结合,也称为**回溯-剪枝法**。 +* 与剪枝相结合,也称为**回溯-剪枝法**。 ### 要素 * **解向量**-问题解的表示: @@ -17,32 +17,33 @@ * **解空间**: 对于一个实例,解向量满足显示约束条件的所有多元组,构成了该实例的一个解空间。 -### 基本方法: +### 基本方法 + 明确定义问题的解空间,将问题的解空间组织成树的结构,通过采用系统的方法隐含搜索解空间树,从而得到问题解。回溯法的基本做法是搜索,是一种组织的井井有条的,能避免不必要搜索的穷举式搜索。搜索策略主要有:深度优先、广度优先、函数优先、广度深度结合。 -节点分支判定条件: +**节点分支判定条件:** * 满足约束条件:分支扩展解向量。 * 不满足约束条件:回溯到当前节点的父结点。 -结点状态: +**结点状态:** * 白结点:尚未访问的节点 * 灰节点:正在访问以该节点为跟的子树。 * 黑节点:以该节点为根的子树遍历完成。 -存储当前索索路径 +**存储当前索索路径** -搜索策略(避免无效搜索) +**搜索策略(避免无效搜索)** * **约束函数**:在扩展节点处减去不满足约束条件的子树。 * **界限函数**:在扩展节点处减去得不到最优解的子树。 ### 回溯法的适用条件 -适用于搜索问题和优化问题 +* 适用于搜索问题和优化问题 必要条件 * **多米诺性质**:叶子节点的解一定满足其父节点。叶子结点为真则父节点一定为真。同理父节点为假则叶子结点一定为假(逆否命题)。用父节点为假的情况进行剪枝操作。 -设P(x1,x2,…,xi)是关于向量的某个性质,那么P(x1,x2,…,xi+1)真蕴含P(x1,x2,…,xi) 为真,即 +* 设P(x1,x2,…,xi)是关于向量的某个性质,那么P(x1,x2,…,xi+1)真蕴含P(x1,x2,…,xi) 为真,即 P(x1,x2,…,xi+1) → P(x1,x2,…,xi) (0