refactor: reconstruct project structure
remove irrelevant file, leaving only my notes during PG exam preparation; add a README to each chapter in the dir '数据结构'; move .png images to a separate dir 'images'; add some discription where necessary.
102
912v1.0.tex
@@ -1,102 +0,0 @@
|
||||
\documentclass[UTF8,12pt]{ctexart}
|
||||
\usepackage{ctex}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{graphicx}
|
||||
\CTEXsetup[format={\Large\bfseries}]{section}
|
||||
\title{\kaishu 912回忆版}
|
||||
\author{by Shine Wong}
|
||||
\date{12/22}
|
||||
|
||||
\begin{document}
|
||||
\maketitle
|
||||
|
||||
\section{数据结构}
|
||||
|
||||
\subsection{判断题}
|
||||
|
||||
\begin{itemize}
|
||||
|
||||
\item[1)]$log^nn = \Omega(n^{logn})$。
|
||||
\item[2)]对一棵AVL树进行插入,则至多会引起$\Omega(logn)$次局部调整操作。
|
||||
\item[3)]对一个理想随机输入的序列进行快速排序,则在平均情况下以及最坏情况下都可以达到$O(logn)$的时间复杂度性能。
|
||||
\item[4)]在理想随机输入的情况下,尽管完全二叉堆的删除操作的最坏时间复杂度有$O(logn)$,平均时间复杂度仅为$O(1)$而已。
|
||||
\item[5)]跳转表每一个节点所对应的塔的平均高度为$O(logn)$。\\
|
||||
\item[6)]采用基于比较的算法,可以在$O(n)$的时间内找出序列的前10\%大的元素。
|
||||
\item[7)]对一有向图进行DFS,共有$k$条边被标记为 BACKWARD,则该图中未必有$k$个环路。\\
|
||||
\item[8)]败者树相对于胜者树,具有更优的渐进时间复杂度性能。\\
|
||||
\item[9)]相对于闭散列,开散列可以更好地利用数据的局部性。\\
|
||||
\item[10)]...remain to be added
|
||||
|
||||
|
||||
\end{itemize}
|
||||
|
||||
\subsection{单向选择题}
|
||||
|
||||
\begin{itemize}
|
||||
|
||||
\item[1)]对一有向无环图,该图的拓扑排序序列恰好是DFS的$\underline{\hbox to 10mm{}}$\\
|
||||
A.\ 被发现的顺序\\
|
||||
B.\ 被发现的逆序\\
|
||||
C.\ 回溯的顺序\\
|
||||
D.\ 回溯的逆序
|
||||
|
||||
\item[2)]如果基数排序底层采用不稳定的排序算法,则所得的结果$\underline{\hbox to 10mm{}}$,并且基数排序的稳定性$\underline{\hbox to 10mm{}}$\\
|
||||
A.\ 不再正确 \ 不再保持\\
|
||||
B.\ 不再正确 \ 仍然保持\\
|
||||
C.\ 仍然正确 \ 不再保持\\
|
||||
D.\ 仍然正确 \ 仍然保持
|
||||
|
||||
\item[3)]逆波兰表达式$Blalala$的结果为2019,则中间缺失的操作符为\\
|
||||
A \ + \\
|
||||
B \ - \\
|
||||
C \ * \\
|
||||
D \ / \\
|
||||
E \ \^ \\
|
||||
F \ !
|
||||
|
||||
\item[4)]对于一个权重分别是1,1,2,3,5,8,13,21的字符集构造Huffman编码树,其中最大的深度为\\
|
||||
A.\ 6\\
|
||||
B.\ 7\\
|
||||
C.\ 8\\
|
||||
D.\ 9
|
||||
|
||||
\item[5)]含有$\underline{\hbox to 10mm{}}$个节点的真二叉树的数量,与2019对括号构成的合法表达式数量相同。\\
|
||||
A.\ 1009\\
|
||||
B.\ 1010\\
|
||||
C.\ 2019\\
|
||||
D.\ 4039
|
||||
|
||||
\item[6)]对一模式串HHBFHHBFHHBFSHH,考虑改进的next数组,则$next[14] - next[0] = \underline{\hbox to 10mm{}}$\\
|
||||
A.\ 2\\
|
||||
B.\ 3\\
|
||||
C.\ 4\\
|
||||
D.\ 5
|
||||
|
||||
\end{itemize}
|
||||
|
||||
\subsection{证明题}
|
||||
|
||||
已知一棵二叉搜索树的先序和后序遍历序列,是否可以构造出它的层次遍历序列?是则给出证明,否则给出一个反例。(5分)
|
||||
|
||||
\subsection{程序设计题}
|
||||
|
||||
给出二叉树节点BinNode的定义如下:\\
|
||||
|
||||
\noindent class BinNode{\\
|
||||
public:\\
|
||||
\indent BinNode* parent;\\
|
||||
\indent BinNode* lc;\\
|
||||
\indent BinNode* rc;\\
|
||||
\indent int lsize;\\
|
||||
|
||||
\indent BinNode* zig(BinNode* x);//绕当前节点顺时针旋转,仍然返回旋转后根节点的左子树\\
|
||||
\indent BinNode* zag(BinNode* x);//绕当前节点顺时针旋转,仍然返回旋转后根节点的右子树\\
|
||||
}
|
||||
|
||||
\begin{itemize}
|
||||
|
||||
\item[]
|
||||
|
||||
\end{itemize}
|
||||
|
||||
\end{document}
|
||||
@@ -1,144 +0,0 @@
|
||||
//Exercise 10.2
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <list>
|
||||
#include <string>
|
||||
#define NUM 10
|
||||
using namespace std;
|
||||
int main(){
|
||||
list<string> strList;
|
||||
string tmp;
|
||||
for (int ix = 0; ix != NUM; ix++) {
|
||||
cin >> tmp;
|
||||
strList.push_back(tmp);
|
||||
}
|
||||
string val;
|
||||
cin >> val;
|
||||
cout << count(strList.begin(), strList.end(), val) << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 10.3
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <numeric>
|
||||
using namespace std;
|
||||
int main(){
|
||||
vector<int> intVec{ 5,4,3,1,2 };
|
||||
cout << accumulate(intVec.cbegin(), intVec.cend(), 0) << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 10.6
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#define NUM 10
|
||||
using namespace std;
|
||||
int main(){
|
||||
vector<int> intVec;
|
||||
fill_n(back_inserter(intVec), 10, 0);
|
||||
for (auto It = intVec.cbegin(); It != intVec.cend(); It++)
|
||||
cout << *It << ' ';
|
||||
cout << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 10.9
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
void print(vector<string> strVec);
|
||||
int main(){
|
||||
vector<string> strVec;
|
||||
string word;
|
||||
while (cin >> word)
|
||||
strVec.push_back(word);
|
||||
print(strVec);
|
||||
sort(strVec.begin(), strVec.end());
|
||||
print(strVec);
|
||||
auto unique_end = unique(strVec.begin(), strVec.end());
|
||||
print(strVec);
|
||||
strVec.erase(unique_end, strVec.end());
|
||||
print(strVec);
|
||||
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void print(vector<string> strVec){
|
||||
if(!strVec.empty()){
|
||||
for (auto It = strVec.cbegin(); It != strVec.cend(); It++)
|
||||
cout << *It << ' ';
|
||||
}
|
||||
cout << endl;
|
||||
}
|
||||
|
||||
//Exercise 10.13
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
using namespace std;
|
||||
void print(vector<string> strVec);
|
||||
bool have_five_char(string word){
|
||||
return word.size() >= 5;
|
||||
}
|
||||
int main(){
|
||||
vector<string> words;
|
||||
string word;
|
||||
while (cin >> word)
|
||||
words.push_back(word);
|
||||
auto end = partition(words.begin(), words.end(), have_five_char);
|
||||
print(words);
|
||||
words.erase(end, words.end());
|
||||
print(words);
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
void print(vector<string> strVec) {
|
||||
if (!strVec.empty()) {
|
||||
for (auto It = strVec.cbegin(); It != strVec.cend(); It++)
|
||||
cout << *It << ' ';
|
||||
}
|
||||
cout << endl;
|
||||
}
|
||||
|
||||
//Exercise 10.14
|
||||
auto func = [](int a, int b){return a+b;};
|
||||
|
||||
//Exercise 10.15
|
||||
int a = 42;
|
||||
auto func = [a](int b){return a+b;};
|
||||
|
||||
//Exercise 10.20
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#define LENGTH 6
|
||||
using namespace std;
|
||||
int main(){
|
||||
vector<string> strVec;
|
||||
string word;
|
||||
while (cin >> word)
|
||||
strVec.push_back(word);
|
||||
auto count = count_if(strVec.cbegin(), strVec.cend(), [](string str) {return str.size() > LENGTH; });
|
||||
cout << count << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 10.21
|
||||
auto func = [arg]() mutable ->bool{
|
||||
if(arg == 0) return True;
|
||||
//else
|
||||
while(arg-- != 0);
|
||||
return False;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <list>
|
||||
#include <string>
|
||||
#include <forward_list>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
|
||||
using namespace std;
|
||||
|
||||
class HasPtr {
|
||||
public:
|
||||
explicit HasPtr(const std::string &s = std::string()) :
|
||||
ps(new std::string(s)), i(0) {}
|
||||
|
||||
//copy constructor
|
||||
HasPtr(const HasPtr& ptr);
|
||||
|
||||
//copy assignment operator
|
||||
HasPtr& operator=(const HasPtr& ptr);
|
||||
|
||||
//destructor
|
||||
~HasPtr(){
|
||||
delete ps;
|
||||
}
|
||||
private:
|
||||
std::string *ps;
|
||||
int i;
|
||||
};
|
||||
|
||||
|
||||
HasPtr::HasPtr(const HasPtr& ptr){
|
||||
ps = new string(*ptr.ps);
|
||||
i = ptr.i;
|
||||
}
|
||||
|
||||
HasPtr& HasPtr::operator=(const HasPtr& ptr){
|
||||
delete ps;
|
||||
ps = new string(*ptr.ps);
|
||||
i = ptr.i;
|
||||
return *this;
|
||||
}
|
||||
|
||||
HasPtr f(HasPtr fp){
|
||||
HasPtr ret = fp;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int main(){
|
||||
HasPtr ptr1("2020");
|
||||
HasPtr ptr2("2019");
|
||||
HasPtr ptr3 = ptr2;
|
||||
forward_list<int> li({ 4, 5, 6 });
|
||||
array<int, 5> arr = { 1 };
|
||||
arr = { 1, 2, 3, 4};
|
||||
arr.assign(1);
|
||||
arr.fill(4);
|
||||
for (auto e : arr)
|
||||
cout << e << " ";
|
||||
cout << endl;
|
||||
|
||||
for (int e : li)
|
||||
cout << e << " ";
|
||||
ptr2 = ptr1;
|
||||
ptr3 = ptr1;
|
||||
f(ptr1);
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
Constructor and Copy Control
|
||||
============================
|
||||
|
||||
## 目录
|
||||
|
||||
+ 构造函数
|
||||
- 初始化列表
|
||||
- 委派构造函数
|
||||
- 转换构造函数
|
||||
+ 拷贝控制
|
||||
- 拷贝构造函数
|
||||
- 拷贝赋值运算符
|
||||
- 析构函数以及三者之间的联系
|
||||
|
||||
## 构造函数
|
||||
|
||||
### 初始化列表
|
||||
|
||||
初始化列表(Constructor Initializer List)其实是我比较常用的,但是我之前的理解有很多误区,这里对它进行一些深入的讨论。
|
||||
|
||||
初始化列表的功能与在构造函数体内直接赋值是没有任何区别的,比如说下面用初始化列表的代码:
|
||||
|
||||
```cpp
|
||||
Sales_data::Sales_data(const string &s, unsigned int cnt, double price): bookNo(s), units_sold(cnt), revenue(cnt * price){}
|
||||
```
|
||||
|
||||
与
|
||||
|
||||
```cpp
|
||||
Sales_data::Sales_data(const string &s, unsigned int cnt, double price){
|
||||
bookNo = s;
|
||||
units_sold = cnt;
|
||||
revenue = price * cnt;
|
||||
}
|
||||
```
|
||||
|
||||
在功能上没有任何区别。我之前也是认为两者完全等价,甚至还更加偏向于第二种写法,因为感觉格式要好看一点。实际上,两者在性能上是不相同的。
|
||||
|
||||
构造函数的执行顺序,是首先执行初始化列表,继而执行构造函数体内的赋值语句。需要注意的是,即使初始化列表为空,编译器仍然会首先对各个成员变量进行默认的初始化操作,因此相对于第一种版本,第二种版本不仅没有省下执行初始化列表的时间,还增加了函数体内的变量赋值的开销。如果成员变量是一个较大的类类型的话,两者的时间开销差别还是蛮大的。
|
||||
|
||||
因此,应该尽量使用初始化列表来对成员变量进行初始化操作,这样可以获得更优的性能。实际上,在一些情况下,必须使用初始化列表。
|
||||
|
||||
对于某一些类型的变量,比如常量变量(const),或者(左值)引用变量,必须在声明的时候同时对它们初始化。如果一个类含有这些成员变量,则必须使用初始化列表对它们进行初始化。此外,如果一个成员变量是没有默认构造函数的类类型,也必须使用初始化列表对它进行初始化。从这里应该可以看出,尽管在多数情况下都可以忽略,但是初始化和赋值其实是两种不同的操作。
|
||||
|
||||
> 初始化列表的执行顺序问题。
|
||||
|
||||
初始化列表的执行顺序并非是按列表中变量的先后次序,而是按照变量在类定义的次序。因此,下面的代码是存在问题的:
|
||||
|
||||
```cpp
|
||||
class X {
|
||||
private:
|
||||
int i;
|
||||
int j;
|
||||
public:
|
||||
// undefined: i is initialized before j
|
||||
X(int val): j(val), i(j) { }
|
||||
};
|
||||
```
|
||||
|
||||
其中,初始化列表中`i`首先被初始化为`j`,然后再对`j`进行初始化,但是对`i`进行初始化时`j`还是未定义的,就有可能导致后续的错误。
|
||||
|
||||
基于上面的讨论,初始化列表的次序最好与成员变量的定义次序相同,并且最好不要用一个成员变量去初始化另一个成员变量,这样就可以避免可能的错误。比如说下面的代码就不存在初始化列表的执行次序问题了:
|
||||
|
||||
```cpp
|
||||
X(int val): j(val), i(val){}
|
||||
```
|
||||
|
||||
### 委派构造函数
|
||||
|
||||
在我写代码的时候,也经常存在这样的需要,即一个构造函数是另一个更加详细的构造函数的子集,或者部分操作。这个时候我就不想全部重新写一遍,看起来也不好看,最好的办法就是在后者中调用前者,这就是委派构造函数(delegating constructor)——这个名字应该是指将一部分工作委派给另一个构造函数完成。
|
||||
|
||||
委派构造函数的语法也很简单,只需要在`:`后面,本来初始化列表的位置调用被委派的构造函数即可。被委派的构造函数(delegated constructor)的初始化列表和函数体都会相继被执行,之后再执行委派构造函数(delegating constructor)的剩余部分。下面的代码就是委派构造函数的一个例子:
|
||||
|
||||
```cpp
|
||||
class Sales_data {
|
||||
public:
|
||||
// nondelegating constructor initializes members from corresponding arguments
|
||||
Sales_data(std::string s, unsigned cnt, double price): bookNo(s), units_sold(cnt), revenue(cnt*price) {}
|
||||
|
||||
// remaining constructors all delegate to another constructor
|
||||
Sales_data(): Sales_data("", 0, 0) {}
|
||||
Sales_data(std::string s): Sales_data(s, 0,0) {}
|
||||
Sales_data(std::istream &is): Sales_data(){ read(is, *this); }
|
||||
};
|
||||
```
|
||||
|
||||
### 转化构造函数
|
||||
|
||||
转化构造函数是一类特殊的构造函数,它只含有一个参数,参数的类型可以是类本身(此时就是拷贝构造函数了,将在后面提到),也可以是其他类型。它的特殊性在于,转化构造函数定义了隐式的类型转化方法,即从参数的类型转化到当前的类类型。
|
||||
|
||||
比如说我可以定义一个`Mystring`类型,它基本沿用`std::string`的基本方法,但是增加一个构造函数,将输入的整型转化为对应的字符串,如下:
|
||||
|
||||
```cpp
|
||||
class Mystring{
|
||||
public:
|
||||
string str;
|
||||
|
||||
Mystring() = default;
|
||||
//converting constructor
|
||||
Mystring(int num): str(to_string(num)){}
|
||||
}
|
||||
```
|
||||
|
||||
这样,在任何期望一个`Mystring`类型变量的位置,都可以传入一个`int`类型,编译器会自动调用上面定义的转换构造函数,从该`int`类型构造出一个`Mystring`类对象。
|
||||
|
||||
例如存在一个外部函数,接收一个`Mystring`类型的对象作为参数,打印出其中的字符串信息:
|
||||
|
||||
```cpp
|
||||
void print(Mystring mystr){
|
||||
cout << mystr.str << endl;
|
||||
}
|
||||
|
||||
print(2020);//this call is perfectly legal
|
||||
```
|
||||
|
||||
完全可以对该函数传入一个整型的变量,此时编译器会自动调用转换构造函数,从该整型变量构造出一个`Mystring`类型变量,作为函数的参数。实际上,在`Mystring`对象初始化的时候,还可以采用下面的方式:
|
||||
|
||||
```cpp
|
||||
Mystring mystr = 2020;//copy initialization
|
||||
```
|
||||
|
||||
这种初始化方式称为拷贝初始化(copy initialization),与直接初始化存在一些区别。在上面的语句中,编译器也会调用转化构造函数,因此该语句与
|
||||
|
||||
```cpp
|
||||
Mystring mystr(2020);//direct initialization
|
||||
```
|
||||
|
||||
这样的直接初始化完全等效。与上面类似,甚至可以直接使用`int`型变量对`Mystring`对象进行赋值。
|
||||
|
||||
从上面也可以看出,转化构造函数是存在一些隐患的,比如说`Mystring str = 2020;`这样的语句看起来多少让人看起来有一些迷惑。为了避免这种歧义性,可以手动避免转化构造函数的隐式类型转化,只需要在转化构造函数的声明前面添加`explicit`关键字即可。
|
||||
|
||||
```cpp
|
||||
class Mystring{
|
||||
public:
|
||||
string str;
|
||||
explicit Mystring(int num): str(to_string(num)){}
|
||||
//other functions
|
||||
```
|
||||
|
||||
这样,就可以避免隐式类型转化了。如果需要将`int`转化为`Mystring`类型,就必须显式调用(explicit)转化构造函数才行。实际上,之前我已经多次遇到转化构造函数了,只是我当时并不知道而已。
|
||||
|
||||
例如`std::string`存在一个将`const char*`转化为`string`的构造函数,因此存在下面的`string`初始化与函数调用语句:
|
||||
|
||||
```cpp
|
||||
string str = "Study hard tomorrow."; //legal
|
||||
int num = stoi("2020"); //legal, implicit convert "2020" to string.
|
||||
```
|
||||
|
||||
同理,`vector`类中存在一个构造函数,可以指定`vector`的初始大小,传入的参数是一个`int`变量。但是并不可以这样初始化`vector`:
|
||||
|
||||
```cpp
|
||||
vector<int> iVec = 2020; //illegal, this constructor is explicit
|
||||
vector<int> iVec(2020); //legal, explicit initialization
|
||||
```
|
||||
|
||||
用一个整型变量给`vector`赋值,这看起来也太怪了,存在不少的歧义性,因此这个构造函数被标记为`explicit`了。
|
||||
|
||||
需要注意的是,`explicit`关键字只能出现在类构造函数声明的位置,如果该构造函数在类声明体外部被定义,则不能再次添加`explicit`关键字了。
|
||||
|
||||
```cpp
|
||||
//error: explicit allowed only on a constructor declaration in a class header
|
||||
explicit Mystring::Mystring(int num){
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## 拷贝控制
|
||||
|
||||
拷贝控制(copy control)是指一个类控制它的对象在拷贝,移动,赋值,析构操作下所进行的活动。其中包含五个函数(操作),即拷贝构造函数(copy constructor),拷贝赋值运算符(copy assignment operator),移动构造函数(move constructor),移动赋值运算符(move assignment operator)以及析构函数(destructor)。下面将对它们分别进行讨论。
|
||||
|
||||
### 拷贝构造函数
|
||||
|
||||
拷贝构造函数是一类特殊的构造函数,它的参数为同类对象的一个引用,该引用通常是一个常量(const)引用,尽管非常量也是可以的。
|
||||
|
||||
```cpp
|
||||
class Mystring{
|
||||
public:
|
||||
string str;
|
||||
|
||||
//default constructor
|
||||
Mystring() = default;
|
||||
//copy constructor
|
||||
Mystring(const Mystring& right);
|
||||
//converting constructor
|
||||
Mystring(int num);
|
||||
}
|
||||
```
|
||||
|
||||
顾名思义,拷贝构造函数的作用,就是在对象被拷贝初始化(copy initialization)的时候被调用,它规定了新的对象应该如何从被拷贝对象进行复制:
|
||||
|
||||
```cpp
|
||||
Mystring str1; //default initialization
|
||||
Mystring str2 = str1; //copy initialization, call copy constructor
|
||||
```
|
||||
|
||||
如果不自己定义拷贝构造函数,那么编译器会自动添加一个默认拷贝构造函数(synthesized copy constructor),它的函数体是,将类的所有可拷贝成员,逐个复制到新的对象当中,即浅拷贝(shallow copy)。
|
||||
|
||||
因此,如果一个类的成员变量包含指针类型,默认拷贝构造函数就可以导致隐患的错误——将一个对象的指针简单拷贝到另一个对象,两个指针指向同一个变量,如果一个对象释放了该指针,则另一个对象也将不再能访问该变量。在这种情况下,就有必要手动定义拷贝构造函数,为目标指针分配一个新的存储空间:
|
||||
|
||||
```cpp
|
||||
class HasPtr{
|
||||
private:
|
||||
std::string *ps;
|
||||
int i;
|
||||
public:
|
||||
HasPtr() = default;
|
||||
//copy constructor
|
||||
HasPtr(const HasPtr& hp): ps(new string(*hp.ps)), i(hp.i){}
|
||||
}
|
||||
```
|
||||
|
||||
在上面的例子中,就为目标对象的指针重新分配了存储空间,即深拷贝(deep copy),从而避免了默认拷贝构造函数可能存在的问题。需要注意的是,这里直接访问了另一个对象`hp`的私有成员变量,这是因为同一类的对象互为友元(friend),因此可以相互访问对象的私有变量。
|
||||
|
||||
下面以一个例子,来说明拷贝初始化过程中发生的操作:
|
||||
|
||||
```cpp
|
||||
std::string str = "Study hard tomorrow.";
|
||||
```
|
||||
|
||||
这个例子在前面转换构造函数的部分已经提过了,它会首先调用转换构造函数,将`const char*`类型的`"Study hard tomorrow"`转换为`std::string`类型,继而再调用拷贝构造函数,拷贝初始化`str`。
|
||||
|
||||
这里的两次构造函数调用看起来多少有些冗余,实际上,在上面这种情况下,一些编译器可能会省略掉(omit)拷贝构造函数的调用,将上面的拷贝初始化优化为直接初始化:
|
||||
|
||||
```cpp
|
||||
std::string str("Study hard tomorrow.");
|
||||
```
|
||||
|
||||
此时就只会调用一次转换构造函数了。
|
||||
|
||||
实际上,除了拷贝初始化以外,拷贝构造函数还在多个场合会被隐式调用:
|
||||
|
||||
+ 函数调用的参数为对象的非引用类型(non-reference type)。此时为了将实参转化为形参传入函数内部,编译器会自动调用拷贝构造函数构造一个新的对象(形参)。
|
||||
+ 函数调用的返回值为对象的非引用类型。此时并不能简单返回函数体内的局部变量,因为函数执行完毕后所有局部变量都将被回收,因此编译器会自动调用拷贝构造函数复制出一个新的对象作为返回结果。
|
||||
+ 初始化数组等复合类型时,将被拷贝的数组的每个对象逐个复制到新的数组当中,此时对拷贝的每个对象编译器都会自动调用拷贝构造函数。
|
||||
+ std的容器进行拷贝初始化的时候,对复制的每一个对象都自动调用拷贝构造函数。
|
||||
|
||||
由于拷贝构造函数有这么多隐式调用的场合,所以绝对不可以用`explicit`关键字去修饰拷贝构造函数。
|
||||
|
||||
现在也可以说明为什么拷贝构造函数的参数必须是同类对象的一个`引用`了。如果拷贝构造函数的参数是非引用类型,在进行拷贝构造时,对于传入的非引用类型的实参(即被拷贝的对象),需要调用拷贝构造函数构造出一个新的形参,这个过程就将不断持续下去,无法终止。
|
||||
|
||||
### 拷贝赋值运算符
|
||||
|
||||
顾名思义,拷贝赋值运算符在对象被赋值时被调用。和拷贝构造函数一样,如果用户没有定义拷贝赋值运算符,编译器将添加默认的拷贝赋值运算符(synthesized copy assignment operator),并且该默认的运算符也是进行浅拷贝。
|
||||
|
||||
因此,如果类含有类型为指针的成员变量,则往往需要自己定义拷贝复制运算符。这里需要注意的是,拷贝赋值运算符的参数通常也是同类对象的常量引用`const reference type`,尽管不是引用类型也是可以的。在这种情况下,进行赋值操作时,编译器会首先调用拷贝构造函数构造出新的形参,随后再执行拷贝赋值运算符内的操作。
|
||||
|
||||
由于c++内置类型的赋值操作,其返回值都是左侧操作数的引用。为了保持与内置类型的一致性,自己定义的拷贝赋值运算符也应该返回左侧操作数的引用。
|
||||
|
||||
```cpp
|
||||
HasPtr& HasPtr::operator=(const HasPtr& hp){
|
||||
delete ps; //free ps first
|
||||
ps = new string(*hp.ps);
|
||||
i = hp.i;
|
||||
return *this; //return a reference to left-hand operand
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,在拷贝赋值运算符中,应该首先释放掉原有指针的空间,防止内存泄露。
|
||||
|
||||
### 析构函数以及三者之间的关系
|
||||
|
||||
关于析构函数我觉得我还是挺熟悉的,这里就不做详细的说明了。
|
||||
|
||||
和上面两个函数一样,如果用户没有自己定义析构函数,编译器将自动为类添加一个默认析构函数(synthesized destructor),它的函数体是空的,如下:
|
||||
|
||||
```cpp
|
||||
HasPtr{
|
||||
private:
|
||||
...
|
||||
public:
|
||||
//other member functions
|
||||
|
||||
~HasPtr(){} //default destructor
|
||||
}
|
||||
```
|
||||
|
||||
但这并不是说默认析构函数什么工作都没有进行。实际上,析构函数体并不直接释放对象的成员变量,对象的成员变量是在执行完析构函数体之后隐式被释放的。因此析构函数体主要进行的应该是指针成员变量的释放,释放动态分配的内存空间。
|
||||
|
||||
下面重点讲解拷贝构造函数,拷贝赋值运算符以及析构函数三者的关系(the rule of three)。
|
||||
|
||||
通常说来,我们并不需要手动定义全部的三个函数,而是视情况定义部分函数就可以了。这里存在一些经验规则:
|
||||
|
||||
> 定义了析构函数的类往往需要同时定义拷贝构造函数和拷贝赋值运算符。
|
||||
|
||||
仍然以上面的`HasPtr`类为例,假设它只定义了析构函数:
|
||||
|
||||
```cpp
|
||||
HasPtr{
|
||||
private:
|
||||
string *ps;
|
||||
int i;
|
||||
public:
|
||||
~HasPtr(){
|
||||
delete ps;
|
||||
}
|
||||
//other member functions
|
||||
}
|
||||
```
|
||||
|
||||
析构函数体内释放`ps`指向的内存空间,随后成员变量`ps`和`i`被隐式回收。考虑一种情况,存在一个外部函数`f`,它的函数体如下所示:
|
||||
|
||||
```cpp
|
||||
HasPtr f(HasPtr hp){
|
||||
HasPtr ret = hp;
|
||||
return ret;
|
||||
}
|
||||
```
|
||||
|
||||
如果传入一个`HasPtr`类型的对象,在实参形参转换时会第一次调用拷贝构造函数,浅拷贝出形参`hp`;在`ret = hp`处调用拷贝赋值运算符,浅拷贝出`ret`;函数返回时,会第二次调用拷贝构造函数,随后两个局部变量`hp`和`ret`都会被销毁。需要注意的是,这里所有对象,他们的成员变量`ps`都指向同一个内存区域,因此在销毁过程中,该内存区域会被释放两次,这是一个内存错误。
|
||||
|
||||
此外,这样一次调用后,原来的实参指向的`string`将失效,因为该区域已经被释放了。从这个例子可以看出,如果定义了析构函数,往往还需要手动定义拷贝构造函数和拷贝赋值运算符。
|
||||
|
||||
> 拷贝构造函数和拷贝赋值运算符往往需要同时被定义,或者同时不被定义。
|
||||
|
||||
换言之,如果定义了一个,往往还需要定义另一个。但是定义了拷贝构造函数和拷贝赋值运算符的类并不一定需要定义析构函数。
|
||||
|
||||
作为总结,理解这里三个函数的关键,在于把握它们各自的调用时机,即在什么语句中,参数的形式是什么样的时候会被调用,这样就可以避免不少潜在的错误。
|
||||
@@ -1,80 +0,0 @@
|
||||
CPP Primer Chp13 Exercises
|
||||
==========================
|
||||
|
||||
> Exercise 13.1: What is a copy constuctor? When is it used?
|
||||
|
||||
拷贝构造函数(copy constructor)是在用拷贝的方式初始化对象的时候,被调用的构造函数,它的形参必须是`const T&`类型,其中`T`为当前类的类名。它会在三种情形下被使用:
|
||||
|
||||
+ 拷贝初始化(copy initialization)。比如说
|
||||
|
||||
```cpp
|
||||
myClass object1();//default construction
|
||||
myClass object2 = object1;//copy initialization
|
||||
```
|
||||
|
||||
+ 函数的形参是一个对象而非对象的引用。此时需要调用拷贝构造函数,从实参拷贝构造一个局部的形参传递到函数体的内部。
|
||||
+ 函数的返回值是一个对象而非对象的引用。由于函数内部的变量具有临时性,函数返回后就会被销毁,因此需要拷贝构造一个新的对象传递给函数的调用者。
|
||||
|
||||
> Exercise 13.2: Explain why the following declaration is illegal:
|
||||
|
||||
```cpp
|
||||
Sales_data::Sales_data(Sales_data rhs);
|
||||
```
|
||||
|
||||
拷贝构造函数的参数必须是对对象的引用。这是因为拷贝构造函数的一个作用,就是在函数的参数为对象的非引用类型时,将实参拷贝构造到形参。如果如上面的声明,对于传入的`rhs`的实参,由于并非是引用类型,需要调用拷贝构造函数将其转化为形参,这个过程将一直持续下去,无法终止。
|
||||
|
||||
> Exercise 13.4: Assuming Point is a class type with a public copy constructor, identify each use of the copy constructor in this program fragment:
|
||||
|
||||
```cpp
|
||||
Point global;
|
||||
Point foo_bar(Point arg)
|
||||
{
|
||||
Point local = arg, *heap = new Point(global);
|
||||
*heap = local;
|
||||
Point pa[4] = { local, *heap };
|
||||
return *heap;
|
||||
}
|
||||
```
|
||||
|
||||
0. `arg`形参并非引用类型,参数传递对应了第一次调用。
|
||||
1. `Point local = arg`。拷贝初始化(copy initialization)时调用拷贝构造函数。
|
||||
2. `heap = new Point(global)`。显式调用拷贝构造函数。
|
||||
3. `Point pa[4] = { local, *heap };`这里有两次调用。
|
||||
4. `return *heap;`返回值并非引用类型,需要调用拷贝构造函数。
|
||||
|
||||
> Given the following sketch of a class, write a copy constructor that copies all the members. Your constructor should dynamically allocate a new string and copy the object to which ps points, rather than copying ps itself.
|
||||
|
||||
```cpp
|
||||
class HasPtr{
|
||||
public:
|
||||
HasPtr(const std::string &s = std::string()):
|
||||
ps(new std::string(s)), i(0){}
|
||||
private:
|
||||
std::string *ps;
|
||||
int i;
|
||||
};
|
||||
```
|
||||
|
||||
```cpp
|
||||
//copy constructor
|
||||
HasPtr::HasPtr(const HasPtr& ptr){
|
||||
ps = new string(*ptr.ps);
|
||||
i = ptr.i;
|
||||
}
|
||||
```
|
||||
|
||||
> Exercise 13.6: What is a copy-assignment operator? When is this operator used? What does the synthesized copy-assignment operator do? When is it synthesized?
|
||||
|
||||
拷贝赋值运算符是一个函数,用于将形参对象的值拷贝到当前对象中;当需要对对象进行赋值时,应该使用拷贝复制运算符;默认的拷贝赋值运算符将传入对象的所有非静态(nonstatic)成员拷贝目标对象当中,可以看出这是一个浅拷贝(shallow copy),当成员变量有指针类型时就会出现问题;如果用户没有定义自己的拷贝赋值运算符,编译器就会生成默认的拷贝赋值运算符。
|
||||
|
||||
> Exercise 13.8: Write the assignment operator for the HasPtr class from exercise 13.5 in. As with the copy constructor, your
|
||||
assignment operator should copy the object to which ps points.
|
||||
|
||||
```cpp
|
||||
HasPtr& HasPtr::operator=(const HasPtr& ptr){
|
||||
delete ps;
|
||||
ps = new string(*ptr.ps)
|
||||
i = ptr.i;
|
||||
return *this;
|
||||
}
|
||||
```
|
||||
@@ -1,174 +0,0 @@
|
||||
Solutions to Exercises in Chapter Seven
|
||||
=======================================
|
||||
|
||||
> Exercise 7.1: Write a version of the transaction-processing program from §1.6 (p. 24) using the Sales_data class you defined for the exercises in §2.6.1 (p. 72).
|
||||
|
||||
[exercise7_1.cpp](exercise7_1.cpp)
|
||||
|
||||
> Exercise 7.2: Add the combine and isbn members to the Sales_data class you wrote for the exercises in § 2.6.2 (p. 76).
|
||||
|
||||
[exercise7_2.h](exercise7_2.h)
|
||||
|
||||
> Exercise 7.3: Revise your transaction-processing program from § 7.1.1 (p.256) to use these members.
|
||||
|
||||
[exercsie7_3.cpp](exercise7_3.cpp)
|
||||
|
||||
> Exercise 7.4: Write a class named Person that represents the name and address of a person. Use a string to hold each of these elements. Subsequent exercises will incrementally add features to this class.
|
||||
|
||||
```cpp
|
||||
#ifndef EXERCISE7_4_H_
|
||||
#define EXERCISE7_4_H_
|
||||
|
||||
#include <string>
|
||||
using std::string;
|
||||
|
||||
struct Person{
|
||||
string name;
|
||||
string address;
|
||||
};
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
> Exercise 7.5: Provide operations in your Person class to return the name and address. Should these functions be const? Explain your choice.
|
||||
|
||||
```cpp
|
||||
#ifndef EXERCISE7_4_H_
|
||||
#define EXERCISE7_4_H_
|
||||
|
||||
#include <string>
|
||||
using std::string;
|
||||
|
||||
struct Person{
|
||||
string name;
|
||||
string address;
|
||||
string getName() const { return name; }
|
||||
string getAddr() const { return address; }
|
||||
};
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
These two functions should be `const`. Because these two functions do not modify the object's members, and `const` objects of this class should be able to call these two functions.
|
||||
|
||||
> Exercise 7.6: Define your own versions of the add, read, and print functions.
|
||||
|
||||
It seems I have done this back in Exercise 7.1.
|
||||
|
||||
> Exercise 7.7: Rewrite the transaction-processing program you wrote for the exercises in § 7.1.2 (p. 260) to use these new functions.
|
||||
|
||||
It seems I have done this back in Exercise 7.1.
|
||||
|
||||
> Exercise 7.8: Why does `read` define its Sales_data parameter as a plain reference and `print` define its parameter as a reference to const?
|
||||
|
||||
Because in `read`, the `Sales_data` object will be modified. While in `print`, this function do not modify the object.
|
||||
|
||||
> Exercise 7.9: Add operations to `read` and `print` Person objects to the code you wrote for the exercises in § 7.1.2 (p. 260).
|
||||
|
||||
```cpp
|
||||
istream& read(istream &is, Person &person){
|
||||
is >> person.name >> person.address;
|
||||
return is;
|
||||
}
|
||||
|
||||
ostream& print(ostream &os, Person const &person){
|
||||
os << person.name << ' ' << person.address;
|
||||
return os;
|
||||
}
|
||||
```
|
||||
|
||||
> Exercise 7.10: What does the condition in the following if statement do?
|
||||
`if (read(read(cin, data1), data2))`
|
||||
|
||||
Take in two objects(data1 and data2) at the same time, and step into the `if` block only when both objects are valid.
|
||||
|
||||
> Exercise 7.11: Add constructors to your Sales_data class and write a program to use each of the constructors.
|
||||
|
||||
[exercise7_11.h](exercise7_11.h)
|
||||
|
||||
[exercise7_11.cpp](exercise7_11.cpp)
|
||||
|
||||
> Exercise 7.12: Move the definition of the Sales_data constructor that takes an istream into the body of the Sales_data class.
|
||||
|
||||
```cpp
|
||||
struct Sales_data {
|
||||
string bookNo;
|
||||
unsigned units_sold = 0;
|
||||
double revenue = 0.0;
|
||||
|
||||
//constructors
|
||||
Sales_data() = default;
|
||||
Sales_data(istream &is){
|
||||
double price;
|
||||
is << bookNo << units_sold << price;
|
||||
revenue = price * units_sold;
|
||||
}
|
||||
Sales_data(string isbn) : bookNo(isbn) {};
|
||||
Sales_data(string isbn, unsigned n, double price) : bookNo(isbn), units_sold(n), revenue(n * price) {};
|
||||
|
||||
string isbn() const { return bookNo; }
|
||||
double avg_price() const { return revenue / units_sold; }
|
||||
Sales_data& combine(Sales_data const &data); //behave same as the `+=` operator, thus return a left-value
|
||||
};
|
||||
```
|
||||
|
||||
> Exercise 7.13: Rewrite the program from page 255 to use the istream constructor.
|
||||
|
||||
```cpp
|
||||
int main(){
|
||||
Sales_data total(cin); // variable to hold the running sum
|
||||
if (cin) { // read the first transaction
|
||||
Sales_data trans(cin); // variable to hold data for the next transaction
|
||||
while (cin) { // read the remaining transactions
|
||||
if (total.isbn() == trans.isbn()) // check the isbns
|
||||
total.combine(trans); // update the running total
|
||||
else {
|
||||
print(cout, total) << endl; // print the results
|
||||
total = trans; // process the next book
|
||||
}
|
||||
trans = Sales_data(cin);
|
||||
}
|
||||
print(cout, total) << endl; // print the last transaction
|
||||
}
|
||||
else { // there was no input
|
||||
cerr << "No data?!" << endl; // notify the user
|
||||
}
|
||||
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
This can be weird, for the `cin` operation is seperated from the `if-condition`.
|
||||
|
||||
> Exercise 7.14: Write a version of the default constructor that explicitly initializes the members to the values we have provided as in-class initializers.
|
||||
|
||||
```cpp
|
||||
struct Sales_data {
|
||||
string bookNo;
|
||||
unsigned units_sold;
|
||||
double revenue;
|
||||
|
||||
//constructors
|
||||
Sales_data(){units_sold = 0, revenue = 0.0;}
|
||||
Sales_data(istream &is){
|
||||
double price;
|
||||
is << bookNo << units_sold << price;
|
||||
revenue = price * units_sold;
|
||||
}
|
||||
Sales_data(string isbn) : bookNo(isbn) {};
|
||||
Sales_data(string isbn, unsigned n, double price) : bookNo(isbn), units_sold(n), revenue(n * price) {};
|
||||
|
||||
string isbn() const { return bookNo; }
|
||||
double avg_price() const { return revenue / units_sold; }
|
||||
Sales_data& combine(Sales_data const &data); //behave same as the `+=` operator, thus return a left-value
|
||||
};
|
||||
```
|
||||
|
||||
But there's a drawback in this manner. For example, in constructor3, `Sales_data(string isbn)`, we also have to explicitly initializes all the members, which causes redundance.
|
||||
|
||||
> Exercise 7.15: Add appropriate constructors to your Person class.
|
||||
|
||||
[exercise7_15.h](exercise7_15.h)
|
||||
|
||||
[exercise7_15.cpp](exercise7_15.cpp)
|
||||
@@ -1,55 +0,0 @@
|
||||
Conlusions on Chapter Seven
|
||||
===========================
|
||||
|
||||
## 7.1 Defining Abstract Data Type
|
||||
|
||||
> 什么是抽象数据类型?
|
||||
|
||||
主要可以参考邓公的网课和教材,基本概念和邓公讲的是一致的。抽象数据类型的关键是两个概念,即封装和数据抽象。为此,需要弄清楚`interface`和`implementation`之间的区别与联系。
|
||||
|
||||
### 7.1.2 Defining Member Functions
|
||||
|
||||
> 首先思考一个问题,当我们调用类的成员函数的时候,如`cppstring Sales_data::isbn() const { return bookNo;}`成员函数是怎么找到当前对象的`bookNo`成员变量的呢?
|
||||
|
||||
`this`指针。成员函数实际上是有一个隐性的变量,即`this`。当调用成员函数的时候,`this`指针会被初始化为指向被调用对象的地址。比如说,当调用
|
||||
|
||||
```cpp
|
||||
total.isbn();
|
||||
```
|
||||
|
||||
的时候,编译器将`this`指针设置为`total`的地址。这就好像`python`一样,所以这个成员函数的调用可以写成
|
||||
|
||||
```cpp
|
||||
Sales_data::isbn(&total);
|
||||
```
|
||||
|
||||
这样,利用`this`指针,就可以随机索引当前对象的任何一个成员变量了。
|
||||
|
||||
> 可以看到,上面的`isbn`成员函数有一个修饰符`const`,这个`const`修饰符有什么作用?
|
||||
|
||||
这个可以有两个理解方法。
|
||||
|
||||
+ 从语言的功能层面,一个`const`的对象只能调用它的标记为`const`的成员函数。这是很自然的,因为编译器会认为没有`const`标记的成员函数会修改对象的成员变量。所以,对于只读的成员函数,尽量都标记为`const`,以便其`const`的对象也可以调用这些函数。
|
||||
|
||||
+ 从变量类型的方面。对于一般的对象,如`Sales_data total`这种,`this`指针的类型为`Sales_data* const`,即`this`指针是`const pointer to non-const object`,在这种情况下,`this`指针是不能指向`const`的对象的。为了指定`this`指针的类型为`const Sales_data* const`,即`const pointer to const object`,就需要在函数的参数列表和具体实现之间加上`const`修饰符。实际上,这也是`const`对象只能调用`const`成员函数在语言层面的原因。
|
||||
|
||||
- class scope and defining a member function outside the class
|
||||
- a function to return `this` object
|
||||
|
||||
### 7.1.3 defining nonmember class-related function
|
||||
- read and print function. io classes
|
||||
|
||||
### 7.1.4 constructors
|
||||
- constructors cannot be `const`
|
||||
- synthesized default constructor and in-class intializers
|
||||
- classes with members of built-in or compound types should not rely on the synthesized default constructor
|
||||
+ so we must define the default constructor for three reasons
|
||||
- =default
|
||||
- constructor initializer list
|
||||
- Defining a Constructor outside the Class Body
|
||||
|
||||
### 7.1.5. Copy, Assignment, and Destruction
|
||||
- Synthesized Versions
|
||||
- cases then Synthesized Versions cannot be used
|
||||
+ classes that allocate resources that reside outside the class objects themselves
|
||||
+ vertor and string members
|
||||
@@ -1,60 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
using namespace std;
|
||||
|
||||
struct Sales_data{
|
||||
string bookNo;
|
||||
unsigned units_sold = 0;
|
||||
double revenue = 0.0;
|
||||
|
||||
string isbn() const { return bookNo; }
|
||||
void combine(Sales_data const &data){
|
||||
units_sold += data.units_sold;
|
||||
revenue += data.revenue;
|
||||
}
|
||||
};
|
||||
|
||||
Sales_data add(Sales_data const &one, Sales_data const &two){
|
||||
Sales_data res;
|
||||
res.bookNo = one.bookNo;
|
||||
res.units_sold = one.units_sold + two.units_sold;
|
||||
res.revenue = one.revenue + two.revenue;
|
||||
return res;
|
||||
}
|
||||
|
||||
istream& read(istream &is, Sales_data &data){
|
||||
is >> data.bookNo;
|
||||
is >> data.units_sold;
|
||||
is >> data.revenue;
|
||||
return is;
|
||||
}
|
||||
|
||||
ostream& print(ostream &os, Sales_data const &data){
|
||||
os << "book number: " << data.bookNo << endl;
|
||||
os << "units sold: " << data.units_sold << endl;
|
||||
os << "total revenue: " << data.revenue << endl;
|
||||
return os;
|
||||
}
|
||||
|
||||
int main(){
|
||||
Sales_data total; // variable to hold the running sum
|
||||
if (read(cin, total)) { // read the first transaction
|
||||
Sales_data trans; // variable to hold data for the next transaction
|
||||
while (read(cin, trans)) { // read the remaining transactions
|
||||
if (total.isbn() == trans.isbn()) // check the isbns
|
||||
total.combine(trans); // update the running total
|
||||
else {
|
||||
print(cout, total) << endl; // print the results
|
||||
total = trans; // process the next book
|
||||
}
|
||||
}
|
||||
print(cout, total) << endl; // print the last transaction
|
||||
}
|
||||
else { // there was no input
|
||||
cerr << "No data?!" << endl; // notify the user
|
||||
}
|
||||
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
//test constructors
|
||||
#include "exercise7_11.h"
|
||||
#include <cassert>
|
||||
|
||||
using std::cin;
|
||||
using std::cout;
|
||||
using std::cerr;
|
||||
|
||||
void test_constructor();
|
||||
|
||||
int main(){
|
||||
test_constructor();
|
||||
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void test_constructor(){
|
||||
//test default constructor
|
||||
Sales_data data1;
|
||||
assert(data1.units_sold == 0);
|
||||
assert(data1.revenue == 0.0);
|
||||
|
||||
//test constructor2
|
||||
Sales_data data2(cin); // cin >> 123456789 2 20.5
|
||||
assert(data2.bookNo == "123456789");
|
||||
assert(data2.revenue == 41.0);
|
||||
assert(data2.units_sold == 2);
|
||||
|
||||
//test constructor3
|
||||
Sales_data data3("123456789");
|
||||
assert(data3.bookNo == "123456789");
|
||||
assert(data3.revenue == 0.0);
|
||||
assert(data3.units_sold == 0);
|
||||
|
||||
//test constructor4
|
||||
Sales_data data4("123456789", 2, 20.5);
|
||||
assert(data4.bookNo == "123456789");
|
||||
assert(data4.revenue == 41.0);
|
||||
assert(data4.units_sold == 2);
|
||||
|
||||
cout << "All tests passed." << endl;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#ifndef EXERCISE7_11_H_
|
||||
#define EXERCISE7_11_H_
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
using std::istream;
|
||||
using std::ostream;
|
||||
using std::endl;
|
||||
using std::string;
|
||||
|
||||
struct Sales_data {
|
||||
string bookNo;
|
||||
unsigned units_sold;
|
||||
double revenue;
|
||||
|
||||
//constructors
|
||||
Sales_data() { units_sold = 0, revenue = 0.0; }
|
||||
Sales_data(istream &is);
|
||||
Sales_data(string isbn) : bookNo(isbn) {};
|
||||
Sales_data(string isbn, unsigned n, double price) : bookNo(isbn), units_sold(n), revenue(n * price) {};
|
||||
|
||||
string isbn() const { return bookNo; }
|
||||
double avg_price() const { return revenue / units_sold; }
|
||||
Sales_data& combine(Sales_data const &data); //behave same as the `+=` operator, thus return a left-value
|
||||
};
|
||||
|
||||
//constructors
|
||||
Sales_data::Sales_data(istream &is){
|
||||
double price;
|
||||
is >> bookNo >> units_sold >> price;
|
||||
revenue = price * units_sold;
|
||||
}
|
||||
|
||||
Sales_data& Sales_data::combine(Sales_data const &data) {
|
||||
units_sold += data.units_sold;
|
||||
revenue += data.revenue;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Sales_data add(Sales_data const &one, Sales_data const &two) {
|
||||
Sales_data res = one;
|
||||
return res.combine(two);
|
||||
}
|
||||
|
||||
istream& read(istream &is, Sales_data &data) {
|
||||
is >> data.bookNo;
|
||||
is >> data.units_sold;
|
||||
is >> data.revenue;
|
||||
return is;
|
||||
}
|
||||
|
||||
ostream& print(ostream &os, Sales_data const data) {
|
||||
os << "book number: " << data.bookNo << endl;
|
||||
os << "units sold: " << data.units_sold << endl;
|
||||
os << "total revenue: " << data.revenue << endl;
|
||||
os << "averge price: " << data.avg_price(); // no endl at the end
|
||||
return os;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,28 +0,0 @@
|
||||
// test construtors
|
||||
#include "exercise7_15.h"
|
||||
#include <cassert>
|
||||
|
||||
using std::cout;
|
||||
using std::cin;
|
||||
using std::endl;
|
||||
|
||||
int main(){
|
||||
//test constructorss
|
||||
Person person1;
|
||||
// here we see non-builtin type(such as string) would not have undefined value
|
||||
assert(person1.getAddr() == string());
|
||||
assert(person1.getName() == string());
|
||||
|
||||
Person person2("shine", "shanghai");
|
||||
// and string can directly be compare with literal character string
|
||||
assert(person2.getName() == "shine");
|
||||
assert(person2.getAddr() == string("shanghai"));
|
||||
|
||||
Person person3(cin); // cin >> shine >> shanghai;
|
||||
assert(person2.getName() == "shine");
|
||||
assert(person2.getAddr() == string("shanghai"));
|
||||
|
||||
cout << "All tests passed." << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#ifndef EXERCISE7_15_H_
|
||||
#define EXERCISE7_15_H_
|
||||
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
using std::string;
|
||||
using std::istream;
|
||||
using std::ostream;
|
||||
|
||||
struct Person;
|
||||
|
||||
istream& read(istream &is, Person &person);
|
||||
ostream& print(ostream &os, Person const &person);
|
||||
|
||||
struct Person {
|
||||
string name;
|
||||
string address;
|
||||
|
||||
//construtors
|
||||
Person() = default;
|
||||
Person(istream& is) { read(is, *this); }
|
||||
Person(string name, string address) : name(name), address(address) {};
|
||||
|
||||
string getName() const { return name; }
|
||||
string getAddr() const { return address; }
|
||||
};
|
||||
|
||||
istream& read(istream &is, Person &person) {
|
||||
is >> person.name >> person.address;
|
||||
return is;
|
||||
}
|
||||
|
||||
ostream& print(ostream &os, Person const &person) {
|
||||
os << person.name << ' ' << person.address;
|
||||
return os;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,44 +0,0 @@
|
||||
#ifndef EXERCISE7_2_H_
|
||||
#define EXERCISE7_2_H_
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
using namespace std;
|
||||
|
||||
struct Sales_data {
|
||||
string bookNo;
|
||||
unsigned units_sold = 0;
|
||||
double revenue = 0.0;
|
||||
|
||||
string isbn() const { return bookNo; }
|
||||
double avg_price() const { return revenue / units_sold; }
|
||||
Sales_data& combine(Sales_data const &data); //behave same as the `+=` operator, thus return a left-value
|
||||
};
|
||||
|
||||
Sales_data& Sales_data::combine(Sales_data const &data){
|
||||
units_sold += data.units_sold;
|
||||
revenue += data.revenue;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Sales_data add(Sales_data const &one, Sales_data const &two) {
|
||||
Sales_data res = one;
|
||||
return res.combine(two);
|
||||
}
|
||||
|
||||
istream& read(istream &is, Sales_data &data) {
|
||||
is >> data.bookNo;
|
||||
is >> data.units_sold;
|
||||
is >> data.revenue;
|
||||
return is;
|
||||
}
|
||||
|
||||
ostream& print(ostream &os, Sales_data const data) {
|
||||
os << "book number: " << data.bookNo << endl;
|
||||
os << "units sold: " << data.units_sold << endl;
|
||||
os << "total revenue: " << data.revenue << endl;
|
||||
os << "averge price: " << data.avg_price(); // no endl at the end
|
||||
return os;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,23 +0,0 @@
|
||||
#include "exercise7_2.h"
|
||||
|
||||
int main() {
|
||||
Sales_data total; // variable to hold the running sum
|
||||
if (read(cin, total)) { // read the first transaction
|
||||
Sales_data trans; // variable to hold data for the next transaction
|
||||
while (read(cin, trans)) { // read the remaining transactions
|
||||
if (total.isbn() == trans.isbn()) // check the isbns
|
||||
total = total.combine(trans); // update the running total
|
||||
else {
|
||||
print(cout, total) << endl; // print the results
|
||||
total = trans; // process the next book
|
||||
}
|
||||
}
|
||||
print(cout, total) << endl; // print the last transaction
|
||||
}
|
||||
else { // there was no input
|
||||
cerr << "No data?!" << endl; // notify the user
|
||||
}
|
||||
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
#ifndef EXERCISE7_4_H_
|
||||
#define EXERCISE7_4_H_
|
||||
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
using std::string;
|
||||
using std::istream;
|
||||
using std::ostream;
|
||||
|
||||
struct Person{
|
||||
string name;
|
||||
string address;
|
||||
string getName() const { return name; }
|
||||
string getAddr() const { return address; }
|
||||
};
|
||||
|
||||
istream& read(istream &is, Person &person){
|
||||
is >> person.name >> person.address;
|
||||
return is;
|
||||
}
|
||||
|
||||
ostream& print(ostream &os, Person const &person){
|
||||
os << person.name << ' ' << person.address;
|
||||
return os;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,500 +0,0 @@
|
||||
#include <list>
|
||||
#include <deque>
|
||||
|
||||
//Exercise 9.2
|
||||
std::list<std::deque<int> > List;
|
||||
|
||||
//Exercise 9.4
|
||||
using std::vector;
|
||||
bool find(vector<int>::iterator front, vector<int>::iterator behind, int target){
|
||||
for(;front<behind;front++){
|
||||
if(*front == target) return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
//Exercise 9.5
|
||||
using std::vector;
|
||||
vector<int>::iterator find(vector<int>::iterator front, vector<int>::iterator behind, int target){
|
||||
for(;front<behind;front++){
|
||||
if(*front == target) return front;
|
||||
}
|
||||
return behind;
|
||||
}
|
||||
|
||||
//Exercise 9.6
|
||||
while(iter1 < iter2)/* ... */ --> while(iter1 != iter2)/* ... */
|
||||
_list DO NOT support `<` operator_
|
||||
|
||||
//Exercise 9.18
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <deque>
|
||||
using namespace std;
|
||||
int main(){
|
||||
deque<string> strdq;
|
||||
string currStr;
|
||||
while(cin >> currStr){
|
||||
strdq.push_back(currStr);
|
||||
}
|
||||
deque<string>::iterator dqIt;
|
||||
for(dqIt = strdq.begin(); dqIt != strdq.end(); dqIt++){
|
||||
cout << *dqIt;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 9.19
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <list>
|
||||
using namespace std;
|
||||
int main() {
|
||||
list<string> strdq;
|
||||
string currStr;
|
||||
while (cin >> currStr) {
|
||||
strdq.push_back(currStr);
|
||||
}
|
||||
list<string>::const_iterator dqIt;
|
||||
for (dqIt = strdq.cbegin(); dqIt != strdq.cend(); dqIt++) {
|
||||
cout << *dqIt << endl;
|
||||
}
|
||||
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 9.20
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <list>
|
||||
#include <deque>
|
||||
using namespace std;
|
||||
int main(){
|
||||
list<int> iList = {8,9,5,3,1,2,6,4,7,0};
|
||||
deque<int> evendq, odddq;
|
||||
for(auto listIt = iList.cbegin(); listIt != iList.cend(); listIt++){
|
||||
//if odd
|
||||
if(*listIt % 2) odddq.push_back(*listIt);
|
||||
else evendq.push_back(*listIt);
|
||||
}
|
||||
|
||||
for(auto dqIt = odddq.cbegin(); dqIt != odddq.cend(); cout << *dqIt << ' ', dqIt++);
|
||||
cout << endl;
|
||||
for(auto dqIt = evendq.cbegin(); dqIt != evnedq.cend(); cout << *dqIt << ' ', dqIt++);
|
||||
cout << endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 9.24
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
int main(){
|
||||
vector<int> emptyVector;
|
||||
try {
|
||||
//cout << *emptyVector.begin() << endl;
|
||||
//cout << emptyVector.front() << endl;
|
||||
//cout << emptyVector[0] << endl;
|
||||
cout << emptyVector.at(0) << endl;
|
||||
}catch(out_of_range err){
|
||||
cout << err.what() << endl;
|
||||
cout << "Press to exit" << endl;
|
||||
system("pause");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 9.26
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
int main(){
|
||||
int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 };
|
||||
vector<int> ivec;
|
||||
list<int> ilst;
|
||||
for(int i = 0; i != 11; i++){
|
||||
ivec.push_back(ia[i]);
|
||||
ilst.push_back(ia[i]);
|
||||
}
|
||||
for(auto vIt = ivec.begin(); vIt != ivec.end();){
|
||||
if (*vIt % 2 == 0) vIt = ivec.erase(vIt); //even
|
||||
else vIt += 1;
|
||||
}
|
||||
for(auto lIt = ilst.begin(); lIt != ilst.end();){
|
||||
if (*lIt % 2) lIt = ilst.erase(lIt); // odd
|
||||
else lIt++;
|
||||
}
|
||||
cout << "result vector:" << endl;
|
||||
for (auto It = ivec.begin(); It != ivec.end(); It++)
|
||||
cout << *It << ' ';
|
||||
cout << "\nresult list" << endl;
|
||||
for (auto It = ilst.begin(); It != ilst.end(); It++)
|
||||
cout << *It << ' ';
|
||||
cout << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 9.27
|
||||
#include <vector>
|
||||
#include <forward_list>
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
int main(){
|
||||
forward_list<int> ilst = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 };
|
||||
auto lIt = ilst.begin();
|
||||
auto prevlIt = ilst.before_begin();
|
||||
for(; lIt != ilst.end();){
|
||||
if (*lIt % 2) lIt = ilst.erase_after(prevlIt); // odd
|
||||
else{
|
||||
lIt++, prevlIt++;
|
||||
}
|
||||
}
|
||||
cout << "\nresult list" << endl;
|
||||
for (auto It = ilst.begin(); It != ilst.end(); It++)
|
||||
cout << *It << ' ';
|
||||
cout << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//Exercise 9.28
|
||||
#include <forward_list>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
using namespace std;
|
||||
forward_list<string> insertStr(forward_list<string> src, string pos, string destStr);
|
||||
int main(){
|
||||
forward_list<string> strLst = { "This", "is", "not", "TRUE", "happiness", "!" };
|
||||
auto resultLst = insertStr(strLst, string("is"), string("true"));
|
||||
for (auto It = resultLst.cbegin(); It != resultLst.cend(); It++)
|
||||
cout << *It << ' ';
|
||||
cout << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
forward_list<string> insertStr(forward_list<string> src, string pos, string destStr){
|
||||
auto It = src.begin();
|
||||
auto prev = src.before_begin();
|
||||
while(It != src.end()){
|
||||
if (*It == pos) {
|
||||
src.insert_after(It, destStr);
|
||||
break;
|
||||
}
|
||||
++prev;
|
||||
++It;
|
||||
}
|
||||
if (It == src.end()) src.insert_after(prev, destStr);
|
||||
return src;
|
||||
}
|
||||
|
||||
//Exercise 9.43
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
string strReplace(string s, string oldVal, string newVal);
|
||||
string::iterator find(string &s, string dest, string::iterator start);
|
||||
int main(){
|
||||
string src("thothothohelloworld"), oldVal("hell"), newVal("world");
|
||||
src = strReplace(src, oldVal, newVal);
|
||||
cout << src << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
string strReplace(string s, string oldVal, string newVal){
|
||||
auto pos = s.begin();
|
||||
while (pos != s.end()) {
|
||||
pos = find(s, oldVal, pos);
|
||||
if (pos == s.end()) break;
|
||||
pos = s.erase(pos, oldVal.size() + pos);
|
||||
pos = s.insert(pos, newVal.begin(), newVal.end());
|
||||
pos += newVal.size();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
string::iterator find(string &s, string dest, string::iterator start){
|
||||
string::iterator pos = start, local, tmp;
|
||||
for(; pos <= s.end() - dest.size(); pos++){
|
||||
tmp = dest.begin();
|
||||
local = pos;
|
||||
while(*tmp == *local){
|
||||
tmp++, local++;
|
||||
if (tmp == dest.end()) return pos;
|
||||
}
|
||||
}
|
||||
return s.end();
|
||||
}
|
||||
|
||||
|
||||
// Exercise 9.44
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
using namespace std;
|
||||
string strReplace(string s, string oldVal, string newVal);
|
||||
int main(){
|
||||
string src("thothotho"), oldVal("tho"), newVal("though");
|
||||
src = strReplace(src, oldVal, newVal);
|
||||
cout << src << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
string strReplace(string s, string oldVal, string newVal){
|
||||
string::size_type pos = 0;
|
||||
while (pos != s.size()) {
|
||||
pos = s.find(oldVal, pos);
|
||||
if (pos == string::npos) break;
|
||||
s.replace(pos, oldVal.size(), newVal);
|
||||
pos += newVal.size();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
//Exercise 9.45
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
using namespace std;
|
||||
string completeName(string name, string prefix, string suffix);
|
||||
int main(){
|
||||
string name, prefix, suffix;
|
||||
cin >> name >> prefix >> suffix;
|
||||
string fullname = completeNam(name, prefix, suffix);
|
||||
cout << fullname << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
string completeName(string name, string prefix, string suffix){
|
||||
name.insert(name.begin(), prefix.begin(), prefix.end());
|
||||
name.append(suffix);
|
||||
return name;
|
||||
}
|
||||
|
||||
//Exercise 9.46: using index and length, and insert function only
|
||||
string completeName(string name, string prefix, string suffix){
|
||||
name.insert(0, prefix);
|
||||
name.insert(name.size(), suffix);
|
||||
return name;
|
||||
}
|
||||
|
||||
//Exercise 9.47
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
using namespace std;
|
||||
int main(){
|
||||
string s("ab2c3d7R4E6");
|
||||
string numbers("0123456789");
|
||||
string::size_type pos = 0;
|
||||
while((pos = s.find_first_of(numbers, pos)) != s.npos){
|
||||
cout << s[pos] << ' ';
|
||||
pos++;
|
||||
}
|
||||
cout << endl;
|
||||
pos = 0;
|
||||
while ((pos = s.find_first_not_of(numbers, pos)) != s.npos) {
|
||||
cout << s[pos] << ' ';
|
||||
pos++;
|
||||
}
|
||||
cout << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 9.49
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
using namespace std;
|
||||
int main(){
|
||||
string ascender("bdfhjklt"), descender("gjpqy");
|
||||
string invalid(ascender.append(descender));
|
||||
vector<string> text;
|
||||
string word;
|
||||
while(cin >> word){
|
||||
text.push_back(word);
|
||||
}
|
||||
string::size_type maxlen = 0;
|
||||
vector<string>::const_iterator pos;
|
||||
for(auto It = text.cbegin();It != text.cend(); It++){
|
||||
if((*It).find_first_of(invalid) == (*It).npos){
|
||||
if ((*It).size() > maxlen) {
|
||||
maxlen = (*It).size();
|
||||
pos = It;
|
||||
}
|
||||
}
|
||||
}
|
||||
cout << *pos << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 9.50.a
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using namespace std;
|
||||
int main() {
|
||||
int N;
|
||||
cin >> N;
|
||||
vector<string> s(N);
|
||||
for (int ix = 0; ix != N; ix++)
|
||||
cin >> s[ix];
|
||||
int sum = 0;
|
||||
for (auto It = s.cbegin(); It != s.cend(); It++) {
|
||||
sum += stoi(*It);
|
||||
}
|
||||
cout << sum << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 9.50.b
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using namespace std;
|
||||
int main() {
|
||||
int N;
|
||||
cin >> N;
|
||||
vector<string> s(N);
|
||||
for (int ix = 0; ix != N; ix++)
|
||||
cin >> s[ix];
|
||||
double sum = 0;
|
||||
for (auto It = s.cbegin(); It != s.cend(); It++) {
|
||||
sum += stod(*It);
|
||||
}
|
||||
cout << sum << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
|
||||
//Exercise 9.51
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#define NUM_OF_MONTH 12
|
||||
using namespace std;
|
||||
class date{
|
||||
private:
|
||||
unsigned year;
|
||||
unsigned month;
|
||||
unsigned day;
|
||||
const char* months[12]{"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December" };
|
||||
const char* alphabet{ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" };
|
||||
public:
|
||||
date(string strDate);
|
||||
date();
|
||||
unsigned get_year(){
|
||||
return year;
|
||||
}
|
||||
//get_month();get_day();
|
||||
void print_date(int format = 0);
|
||||
};
|
||||
|
||||
//default constructor
|
||||
date::date(){
|
||||
year = 1998;
|
||||
month = 1;
|
||||
day = 1;
|
||||
}
|
||||
|
||||
//constructor taking a string
|
||||
date::date(string strDate){
|
||||
string strMonth, strYear, strDay;
|
||||
if (strDate.find(' ') != strDate.npos) {
|
||||
strMonth = strDate.substr(0, strDate.find(' '));
|
||||
int ix;
|
||||
try {
|
||||
for (ix = 0; ix != NUM_OF_MONTH; ix++) {
|
||||
if (!strMonth.compare(months[ix]) || !strMonth.compare(string(months[ix], 3))) {
|
||||
month = ix + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ix == NUM_OF_MONTH) throw runtime_error("input error: invalid month");
|
||||
strYear = strDate.substr(strDate.size() - 4);
|
||||
if (strYear.find_first_of(alphabet) != strYear.npos) throw runtime_error("input error: invalid year");
|
||||
else year = stoi(strYear);
|
||||
strDay = strDate.substr(strDate.find(' ') + 1, strDate.size() - 6);
|
||||
if (strDay.find_first_of(alphabet) != strDay.npos) throw runtime_error("input error: invalid day");
|
||||
else day = stoi(strDay);
|
||||
}
|
||||
catch(runtime_error err){
|
||||
cout << err.what() << endl;
|
||||
cout << "continue or not(y/n): ";
|
||||
char c;
|
||||
cin >> c;
|
||||
if (c == 'n' || c == 'N') exit(0);
|
||||
}
|
||||
}else{//format 1/1/1998
|
||||
try {
|
||||
strMonth = strDate.substr(0, strDate.find('/'));
|
||||
if (strMonth.find_first_of(alphabet) != strMonth.npos) throw runtime_error("input error: invalid month");
|
||||
else month = stoi(strMonth);
|
||||
strYear = strDate.substr(strDate.size() - 4);
|
||||
if (strYear.find_first_of(alphabet) != strYear.npos) throw runtime_error("input error: invalid year");
|
||||
else year = stoi(strYear);
|
||||
strDay = strDate.substr(strDate.find('/') + 1, strDate.size() - 5);
|
||||
if (strDay.find_first_of(alphabet) != strDay.npos) throw runtime_error("input error: invalid day");
|
||||
else day = stoi(strDay);
|
||||
}
|
||||
catch(runtime_error err){
|
||||
cout << err.what() << endl;
|
||||
cout << "continue or not(y/n): ";
|
||||
char c;
|
||||
cin >> c;
|
||||
if (c == 'n' || c == 'N') exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//print current date with various formats
|
||||
void date::print_date(int format){
|
||||
switch(format){
|
||||
default:
|
||||
case 0:
|
||||
cout << months[month-1] << ' ' << day << ", " << year << endl;;
|
||||
break;
|
||||
case 1:
|
||||
cout << string(months[month-1], 3) << ' ' << day << ", " << year << endl;
|
||||
break;
|
||||
case 2:
|
||||
cout << month << '/' << day << '/' << year << endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Exercise 9.52
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <stack>
|
||||
using namespace std;
|
||||
int main(){
|
||||
string expr{ "This is (Mooophy(awesome)((((wooooooooo))))) and (ocxs) over" };
|
||||
const char repl = '#';
|
||||
stack<char> charStack;
|
||||
int seen = 0;
|
||||
for(auto It = expr.cbegin(); It != expr.cend(); It++){
|
||||
charStack.push(*It);
|
||||
if (*It == '(') seen += 1;
|
||||
if(*It == ')' && seen > 0){
|
||||
seen -= 1;
|
||||
while (charStack.top() != '(') charStack.pop();
|
||||
charStack.pop();
|
||||
charStack.push(repl);
|
||||
}
|
||||
}
|
||||
//output the result
|
||||
string output;
|
||||
for (; !charStack.empty(); charStack.pop())
|
||||
output.insert(output.begin(), charStack.top());
|
||||
cout << output << endl;
|
||||
system("pause");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
Conclusions on Sequential Containers
|
||||
====================================
|
||||
|
||||
## 目录
|
||||
+ 综述
|
||||
+ 线性容器基本操作
|
||||
- 构造函数
|
||||
- 拷贝/赋值/交换
|
||||
- 插入与删除
|
||||
- 访问
|
||||
- 容量
|
||||
- 遍历
|
||||
+ string的额外操作
|
||||
+ 容器适配器
|
||||
- 栈
|
||||
- 队列
|
||||
- 优先队列
|
||||
|
||||
## 综述
|
||||
|
||||
这篇文章讨论c++标准库(stl)中的线性容器(`Sequential Container`),它们包括`vector`,`deque`,`list`,`forward_list`,`array`以及`string`。每个容器提供的成员函数之间具有一些共性,同时也会提供一些特殊的成员函数,理解它们的关键在于了解每个容器背后的基本原理(这应该是数据结构的基础知识),以及相关成员函数的设计思想。
|
||||
|
||||
六个线性容器又可以分为两个阵营,向量、双端队列、 string 和 array是第一个阵营,它们在物理上都是连续存储的;列表和单向列表是第二个阵营,它们是采用动态的存储策略。存储策略的不同会导致不同容器的静态操作与动态操作具有不同的性能,因此提供的成员函数也有区别,例如 vector 并不提供在容器的前端插入或者删除元素的成员函数`push_front`和`pop_front`,因为这将需要`O(n)`的时间复杂度。在后面还将详细地阐述这一点。
|
||||
|
||||
此外,六个容器中还有两朵奇葩,即 array 和单向列表。 array 是一个容量固定的容器,而所有其他容器都是可以动态扩容的,这决定了 array 的许多成员函数都与其他容器不同,例如不提供插入或者删除元素的接口;单向列表的设计思想是要保证最优的性能,因此单向列表削减了一些成员变量,比如单向列表不提供`size`接口,因为`O(1)`的`size`接口需要维护一个额外的 size 变量,在每次插入或者删除时都需要对该变量进行修改,这会在一定程度上降低性能。此外,由于单向列表结构的特殊性,它提供的插入和删除接口都与其他容器不同,这也会在后面详细阐述。
|
||||
|
||||
在实际应用中,对字符串的处理是相当常见的,为了使这部分工作变得简单,string 提供了大量额外的成员函数,本文将对其中比较常用的进行介绍。
|
||||
|
||||
最后,本文介绍了容器适配器的概念,并且就其中最常见的三个,即栈,队列,优先队列进行说明。
|
||||
|
||||
## 线性容器基本操作
|
||||
|
||||
任何一个线性容器都包含一些共有的基本操作,包括初始化,拷贝/赋值/交换,插入元素,删除元素,访问元素,遍历整个容器,以及获得容器的容量信息。下面将分别进行阐述。
|
||||
|
||||
### 初始化
|
||||
|
||||
线性容器提供了多种构造函数:
|
||||
|
||||
+ 默认构造函数。
|
||||
+ 拷贝构造函数。从另一个**同类型**的容器进行拷贝。这里需要注意的是,对于 array ,它的容量也是它的类型,因此 array 只能从相同元素类型并且容量也相同的 array 进行拷贝构造。
|
||||
|
||||
```cpp
|
||||
Container<type> c1;
|
||||
Container<type> c2(c1); //c2 is a copy of c1
|
||||
|
||||
//for array type
|
||||
array<int, 6> arr1; //arr1 has 6 elements with value 0
|
||||
array<int, 6> arr2 = arr1; //arr2 is a copy of arr1
|
||||
array<int, 8> arr3 = arr2; //error, different size
|
||||
```
|
||||
|
||||
+ 指定容量的构造函数,该构造函数还可以指定每个元素的初始值。需要注意的是这个一个 `explicit` 的构造函数。显然 array 不提供该构造函数,否则就会有两个容量信息了。
|
||||
|
||||
```cpp
|
||||
vector<int> ivec1(10); //ivec1 has 10 elements valued 0
|
||||
vector<int> ivec2 = 10; //error, explicit constructor
|
||||
vector<int> ive3(10, 5); //ivec3 has 10 elements valued 5
|
||||
|
||||
forward_list<float> flist(5);
|
||||
deque<double> ddeque(20, 3.14);
|
||||
```
|
||||
|
||||
+ 初始化列表(`initializer list`)。需要注意,对于 array ,列表中的元素个数不可以多于 array 的容量,当列表元素少于 array 的容量时,其余的元素被默认初始化,这其实和内置类型的数组是一致的。
|
||||
|
||||
```cpp
|
||||
array<int, 5> arr1 = {1, 2, 3, 4, 5};
|
||||
array<int, 5> arr2 = {1, 2, 3, 4, 5, 6}; //error
|
||||
array<int, 5> arr3 = {1}; //other elements are default initialized to 0
|
||||
|
||||
forward_list<float> f_list = {1.2, 3.4, 5.6};//f_list has size 3
|
||||
deque<double> ddeque = {5.0, 6.9}; //ddeque has size 2
|
||||
```
|
||||
|
||||
+ 通过两个**其他容器**的迭代器指定拷贝的范围。这种方法使得可以从其他类型的容器拷贝。array 是不支持这种方式的,因为指定的范围难以保证与 array 的容量一致。
|
||||
|
||||
```cpp
|
||||
list<int> ilist = {1, 2, 3, 4, 5};
|
||||
vector<int> ivec(ilist.begin(), ilist.end()); //valid, ivec = {1, 2, 3, 4, 5}
|
||||
deque<int> ideque(ivec.begin() + 1, ivec.end());//valid, ideque = {2, 3, 4, 5}
|
||||
```
|
||||
|
||||
### 拷贝/赋值/交换
|
||||
|
||||
stl中提供了三种拷贝/赋值的方式:
|
||||
|
||||
+ 拷贝运算符(copy assignment operator)。和构造函数一样,这种方式也只能支持同类型容器之间的拷贝(或者赋值)。
|
||||
|
||||
```cpp
|
||||
array<int, 10> arr1 = {42};
|
||||
array<int, 10> arr2 = {1, 2};
|
||||
array<int, 11> arr3;
|
||||
arr2 = arr1; //valid, replace elements in arr2
|
||||
arr3 = arr2; //error, different size
|
||||
|
||||
vector<int> ivec1 = {1, 2, 3, 4, 5};
|
||||
vector<int> ivec2(10, 1);
|
||||
ivec2 = ivec1; //valid
|
||||
```
|
||||
|
||||
+ `assign`成员函数。`assign`成员函数有多种重载形式,它们的参数其实就类似于对应的构造函数的参数,比如说可以通过两个迭代器来指定拷贝的范围,同样这实现了在不同类型容器之间的拷贝;可以传入一个初始化列表,也可以传入一个正整数代表 size,以及这些元素的值。显然,array 并不提供`assign`成员函数,但是有一个类似的`fill`函数,可以对所有元素赋值。
|
||||
|
||||
```cpp
|
||||
vector<int> ivec = {1, 2, 3, 4, 5};
|
||||
list<int> ilist1(10, 1), ilist2, ilist3;
|
||||
ilist1.assign(ivec.begin(), ivec.end()); //ilist1 now has 5 elements
|
||||
ilsit2.assign({1, 2, 3, 4, 5}); //so does ilist2
|
||||
ilist3.assign(5, 1); //and ilist3
|
||||
|
||||
array<int, 5> arr = {1};
|
||||
arr.assign(ivec.begin(), ivec.end()); //error
|
||||
arr.assign(5, 2); //error
|
||||
arr.assign({1, 2, 3, 4, 5}); //error
|
||||
arr.fill(5); //arr has 5 elements valued 5
|
||||
```
|
||||
|
||||
+ 通过一个初始化列表进行拷贝。对于 array 而言,初始化列表的长度不能超过 array 的容量,当列表元素少于容量时,其他元素被默认初始化。这与构造函数也是一致的。
|
||||
|
||||
```cpp
|
||||
vector<int> ivec(5, 1);
|
||||
ivec = {2, 3, 4, 5}; //ivec has 4 elements
|
||||
|
||||
array<int, 5> arr;
|
||||
arr = {1, 2, 3 ,4, 5}; //valid
|
||||
arr = {1, 2, 3, 4, 5, 6}; //error, to many elements
|
||||
```
|
||||
|
||||
此外,stl 还提供了**交换**两个**同类型**容器的方法`swap`。对于除了 array 以外的其他容器,`swap`并不进行元素的拷贝,赋值或者移动等,而是简单地交换两个容器的指针而已,因此可以保证`O(1)`的时间复杂度。然而,交换两个 array 却要做实质的元素移动操作,因此交换 array 的时间复杂度为`O(n)`。
|
||||
|
||||
```cpp
|
||||
vector<string> svec1(10);
|
||||
vector<string> svec2(24);
|
||||
swap(svec1, svec2); //swap two vectors in constant time
|
||||
svec1.swap(svec2); //same as the above statement
|
||||
|
||||
array<int, 5> arr1 = {1}, arr2 = {2};
|
||||
array<int, 6> arr3;
|
||||
swap(arr1, arr2); //O(n) swap
|
||||
swap(arr1, arr3); //error: different size
|
||||
```
|
||||
|
||||
### 插入与删除元素
|
||||
|
||||
除 array 外的其他容器都提供了若干插入与删除元素的成员函数,而 array 由于它的定长特性,并不允许元素的插入与删除。除了单向列表以外,这些插入和删除的成员函数都有相同的接口形式,因而更加便于记忆与把握;而单向列表由于它结构上的特殊性,提供了独特的一套插入删除接口,也会在后面涉及到。
|
||||
|
||||
> `push_back`与`pop_back`
|
||||
|
||||
`push_back`与`pop_back`的设计思想是提供一个高效地在容器后端插入或者删除元素的接口,它的时间复杂度仅为`O(1)`。显然,单向列表并不提供这一接口,因为在单向列表后端插入或删除元素需要`O(n)`的时间复杂度。另外四个容器,即向量,双端队列以及 string以及列表都提供这两个成员函数。
|
||||
|
||||
需要注意的是,`push_back`与`pop_back`都没有返回值。
|
||||
|
||||
> `push_front`与`pop_front`
|
||||
|
||||
与`push_back`和`pop_back`类似,`push_front`与`pop_front`提供在`O(1)`时间内在容器的**前端**插入或者删除元素的接口。对于向量与 string 而言,在前端插入或者删除元素涉及到其他所有元素的移动,时间复杂度为`O(n)`,因而它们并不提供这一接口。双向队列,列表与单向列表都提供这两个成员函数。
|
||||
|
||||
这两个函数也没有返回值。
|
||||
|
||||
> `insert`与`erase`
|
||||
|
||||
`insert`与`erase`是更加一般地插入或者删除元素的接口,可以在任意位置进行插入或者删除。
|
||||
|
||||
对于`insert`而言,有若干接口形式,所有这些接口的第一个参数都是一个迭代器指示的位置信息,表示元素应该被插入的位置。需要注意的是,元素实际上是被插入到该迭代器的前面一个位置,这样当传入的参数是`c.begin()`的时候就可以将新的元素插入到容器的最前端,传入的参数时`c.end()`的时候,将新的元素插入到容器的最后端。因此,`insert`函数,实际上是`insert_before`接口。
|
||||
|
||||
+ `insert(p, t)`:将新的元素`t`插入到位置`p`的前面。
|
||||
+ `insert(p, begin, end)`:将**另一容器**从迭代器`begin`到`end`的元素插入到位置`p`的前面。
|
||||
+ `insert(p, n, t)`:将n个相同的元素`t`插入到位置`p`的前面。
|
||||
+ `insert(p, il)`:将初始化列表`il`中的元素插入到位置`p`的前面。
|
||||
|
||||
需要注意的是,`insert`返回指向第一个被插入元素的迭代器,如果没有元素被插入的话(比如`begin = end`,则返回第一个参数。利用`insert`的返回值可以实现反复将新的元素插入到容器的最前端
|
||||
|
||||
```cpp
|
||||
list<string> slist;
|
||||
auto it = slist.begin();
|
||||
while(cin >> word){
|
||||
it = slist.insert(it, word);
|
||||
}
|
||||
```
|
||||
|
||||
上面的代码与下面这个版本完全等效
|
||||
|
||||
```cpp
|
||||
list<string> slist;
|
||||
while(cin >> word)
|
||||
slist.push_front(word);
|
||||
```
|
||||
|
||||
对于`erase`而言,也提供了两种接口:
|
||||
|
||||
+ `erase(p)`:删除迭代器`p`指向的元素。
|
||||
+ `erase(begin, end)`:删除迭代器`begin`到`end`范围内的所有元素。
|
||||
|
||||
两种接口都返回指向被删除的最后一个元素的后一个位置的迭代器。除此以外,stl 还提供了`clear`成员函数,可以将所有元素清除,其效果等价于
|
||||
|
||||
```cpp
|
||||
c.clear();//is equivalent to
|
||||
c.erase(c.begin(), c.end());
|
||||
```
|
||||
|
||||
> `emplace`家族
|
||||
|
||||
除了`insert`家族以外,`emplace`家族也可以实现将元素插入到容器中的功能。与`insert`不同,`emplace`的作用并非将传入的元素拷贝到容器中,而是**构造**一个新的元素然后再插入到容器中,因此它的参数并非容器的`element_type`,而是`element_type`的构造函数的参数。`emplace`成员函数首先调用元素类型的构造函数,然后将新构造的元素插入到容器中。
|
||||
|
||||
与`insert`相同,`emplace`家族也提供了相似的三种成员函数:
|
||||
|
||||
+ `emplace_back`:将新构造的元素插入到容器的尾部,显然单向列表并不提供该接口。
|
||||
+ `emplace_front`:将新构造的元素插入到容器的首部,显然向量和 string 不提供该接口。
|
||||
+ `emplace`:将新构造的元素插入到容器中的任何位置,与`insert`一样,该接口实际上是`emplace_before`。
|
||||
|
||||
`emplace`家族的参数与`insert`家族是类似的,只不过`emplace`并不支持同时插入多个元素,它们的返回值也是一样的,即指向被插入的(第一个)元素的迭代器。
|
||||
|
||||
> 单向列表的插入与删除
|
||||
|
||||
此前所提及的`insert`,`erase`以及`emplace`接口,都不被单向列表实现。这是由单向列表结构的特殊性决定的。
|
||||
|
||||
考虑一种情况,单向列表使用`insert(p, t)`插入元素`t`,为了将元素插入到迭代器`p`的前面,不得不从头遍历单向列表,找到迭代器`p`前面一个位置,然后才能调整指针完成实质的插入操作,该算法的时间复杂度为`O(n)`,而列表这种动态结构的插入与删除理论上是只需要`O(1)`的时间的,该性能显然不能令人满意。对于元素的删除也有同样的问题,因为将迭代器指示的元素释放后,还需要调整它前后的指针,为了找到被删除元素的前驱,也需要耗费`O(n)`的时间。
|
||||
|
||||
容易看到,上面操作非常低效,实质上是因为找到某个元素的前驱需要耗费大量的时间。因此单向列表定义了特殊的插入与删除成员函数,规避了这种无意义的查找,即`insert_after`,`erase_after`以及`emplace_after`。
|
||||
|
||||
对于`insert_after`而言,由于在插入到迭代器指示位置的后面,迭代器本身就是被插入元素的前驱,在`O(1)`时间内就能实现指针的修改。对于`erase_after`也是同样的道理,被删除元素的前驱就是传入的参数,因此可以在`O(1)`时间内完成指针的修改。
|
||||
|
||||
单向列表提供的插入与删除接口,与`insert`,`erase`完全相同:
|
||||
|
||||
+ `insert_after(p, t)`
|
||||
+ `insert_after(p, begin, end)`
|
||||
+ `insert_after(p, n, t)`
|
||||
+ `insert_after(p, it)`
|
||||
|
||||
所有这些接口都返回指向被插入的**最后**一个元素的迭代器,需要注意`insert`是返回指向被插入的第一个元素的迭代器。如果没有元素被插入的话,仍然是返回`p`。
|
||||
|
||||
`erase_after`的两个接口
|
||||
|
||||
+ `erase_after(p)`
|
||||
+ `erase_after(begin, end)`:将从`begin`之后的第一个元素,到(不包括)`end`之间的元素删除。
|
||||
|
||||
它们的返回值仍然是指向被删除元素的后一个元素的迭代器。
|
||||
|
||||
在这几个接口的语义下,会产生一些新的问题,比如如果想要插入到单向列表的最前面,应该怎么实现呢?因此单向列表还定义了特殊的迭代器`forward_list::before_begin`以及`forward_list::cbefore_begin`,利用这两个迭代器就可以插入或者删除最前面的元素了。
|
||||
|
||||
### 元素访问
|
||||
|
||||
对线性容器中的元素访问有四种不同的方式:
|
||||
|
||||
+ `c.front()`:返回容器中第一个元素的**引用**;
|
||||
+ `c.back()`:返回容器中最后一个元素的**引用**;
|
||||
+ `c[n]`:返回容器中第 n 个元素的引用;
|
||||
+ `c.at(n)`:返回容器中第 n 个元素的引用。
|
||||
|
||||
并非所有六个容器都支持这些方式,对这一点的理解还是要基于容器本身的结构。比如单向列表并不支持 `c.back()` 这种方式,因为需要 `O(n)` 的时间才能访问到最后一个元素;同理,不支持随机访问的结构,即单向列表与列表,都不支持 `c[n]`以及`c.at(n)` 这两个接口,因为这两个接口都是基于容器内元素的物理连续性,从而可以利用偏移量 n 快速计算出元素的地址。
|
||||
|
||||
### 容量
|
||||
|
||||
在前面已经提到过,除了 forward_list 以外,所有其他线性容器都支持复杂度为 `O(1)` 的 `size` 接口,返回当前容器的元素个数。此外,string 还有一个 `length` 成员函数,它的作用与 `size` 是完全相同的。
|
||||
|
||||
此外,所有线性容器都提供了 `resize(n)` 接口,使得容器含有 n 个元素。如果容器原先的元素个数大于 n,则将后面的元素删除;而如果原先的元素个数小于 n,新添加的元素被默认初始化。此外,还可以通过 `resize(n, t)` 使得新添加的元素被初始化为 `t`。
|
||||
|
||||
对于 vector 和 string 而言,需要更加精细的容量控制。这是因为 forward_list 与 list 都是采用非连续的存储策略,插入元素时只需要分配一个元素的存储空间即可;而 vector 和 string 中元素个数一旦超过了为其预先分配的空间,则不得不重新在内存中为它开辟一个更大的空间,并且将元素依次拷贝过去,整个过程的时间复杂度为`O(n)`。
|
||||
|
||||
因此,vector 和 string 需要同时管理 size 和 capacity 信息。两者都可以利用`c.reserve(n)`成员函数,预先在内存中开辟大小为 n (个元素)的空间,执行后有 `size = 0, capacity = n * sizeof(element_type)`,这样在插入 n 个元素前,就都不会进行自动扩容了。
|
||||
|
||||
需要注意的是,deque 是没有这样的问题的,因为它的底层结构并非完全连续的,而是由若干连续内存区段组成的,这些段之间并非连续。因此,deque 自动扩容时,只需分配一个区段的存储空间即可,并不会进行元素的拷贝,开销相对较小。
|
||||
|
||||
|
||||
### 遍历
|
||||
|
||||
遍历则相对简单。所有的线性容器(乃至关联容器)都提供了一组相同的接口:
|
||||
|
||||
+ `c.begin()`, `c.end()`。
|
||||
+ `c.cbegin()`, `c.cend()`。
|
||||
+ `c.rbegin()`, `c.rend()`。用于反向遍历容器的场合,显然 forward_list 并不提供该接口。
|
||||
+ `c.crbegin()`, `c.crend()`。返回常量反向迭代器。
|
||||
|
||||
## string 的额外操作
|
||||
|
||||
stl 为 string 提供了若干额外的操作接口,在相当程度上简化了 string 的操作。
|
||||
|
||||
+ substr(begin, len)
|
||||
+ extra constructor
|
||||
- string str = "c style string";
|
||||
- string str
|
||||
+ find, rfind, find_first_of, find_last_of, find_first_not_of, find_last_not_of
|
||||
- find(pattern, pos)
|
||||
+ append, replace
|
||||
+ convert
|
||||
- to_string
|
||||
- stod(double)
|
||||
- stoi(integer)
|
||||
+ isdigit, isalpha, isalnum, iscontrl
|
||||
+ tolower(c), toupper(c)
|
||||
|
||||
## 容器适配器
|
||||
|
||||
栈和队列默认是使用 deque 实现的,而优先队列默认是使用 vector 来实现。三个容器适配器都支持的操作:
|
||||
|
||||
+ push()
|
||||
+ emplace()
|
||||
+ pop()
|
||||
+ size()
|
||||
+ empty
|
||||
|
||||
获取适配器中顶端(末端)元素
|
||||
|
||||
+ stack.top()
|
||||
+ queue.front(), queue.back()
|
||||
+ priority_queue.top(), priority_queue.front()
|
||||
|
||||
实际上,`algorithm` 头文件中提供了一组与堆相关的函数,我感觉比 priority_queue 灵活很多:
|
||||
|
||||
+ `make_heap(begin, end, cmp)`: 对一个 vector 进行建堆。
|
||||
+ `pop_heap(begin, end, cmp)`: 将堆顶元素与最后一个元素交换,然后对新的堆顶做下滤操作。堆的容量会减少1,但是 vector 的容量不变,需要实质地调用 c.pop_back() 才能删除原先的堆顶。
|
||||
+ `push_heap(begin, end, cmp)`:调用 c.push_back() 后调用该函数,对插入到尾部的元素进行上滤操作。
|
||||
+ `sort_heap(begin, end, cmp)`:堆排序,默认是升序。
|
||||
@@ -1,13 +0,0 @@
|
||||
Some Common Bugs
|
||||
================
|
||||
|
||||
为了更好的debug,我觉得还是有必要把平时出现的有点难度的bug记录下来,以让自己知道你自己写的究竟是些啥???
|
||||
|
||||
> 在调用了构造函数,进行了必要的初始化,之后的操作却出现了空指针?
|
||||
|
||||
因为我嵌套调用了构造函数。在构造函数中调用其他构造函数,只会产生一个匿名的对象,并不会帮助你初始化当前的对象。
|
||||
|
||||
> 写了一个`read`函数,接受一个`istream`以及一个对象作为参数,即`read(istream is, myClass object)`。在函数体中利用输入流将数据读入对象中,并返回该`istream`作为进一步的判断或者使用。结果`object`没有得到赋值?!
|
||||
|
||||
|
||||
因为我是个傻逼。传入的`object`肯定要是引用啊。
|
||||
@@ -1,38 +0,0 @@
|
||||
## exception
|
||||
|
||||
+ 基本结构`try-catch`
|
||||
|
||||
try{
|
||||
expression
|
||||
}catch(errorType err){
|
||||
expression
|
||||
}catch(errorType err){
|
||||
expression
|
||||
}...
|
||||
+ 文件支持
|
||||
|
||||
#include <stdexcept>
|
||||
//#include <exception>
|
||||
using std::runtime_error
|
||||
+ 具体使用方法
|
||||
- try block内放可能会产生异常的语句,并抛出(throw)相应的异常
|
||||
|
||||
try{
|
||||
if(...) throw runtime_error("fatal error: dividing zero");
|
||||
if(...) throw ...
|
||||
//else
|
||||
normal expression
|
||||
}
|
||||
- 在catch中检查可能会出现的异常,若异常与catch的异常类型对应,则进入相应的异常处理程序
|
||||
|
||||
try{
|
||||
...
|
||||
}catch(runtime_error err){
|
||||
cout << err.what() << endl;//err.what()返回抛出异常时初始化的字符串
|
||||
//other handling
|
||||
cout << "continue or not(y/n): ";
|
||||
char c;
|
||||
cin >> c;
|
||||
if(c == 'n' || c == 'N') break;
|
||||
//else continue
|
||||
}
|
||||
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,503 +0,0 @@
|
||||
线性回归问题总结
|
||||
===============
|
||||
|
||||
## 目录
|
||||
+ 摘要
|
||||
+ 线性回归概述
|
||||
- 假设函数与平方损失函数
|
||||
- 为什么是最小二乘法
|
||||
- 多项式回归
|
||||
+ 梯度下降法
|
||||
- 一般描述
|
||||
- 学习率的选择
|
||||
- 在多变量下存在的问题
|
||||
+ 特征缩放
|
||||
+ 均值归一化
|
||||
- 局部最优
|
||||
+ 平方损失函数是凸函数
|
||||
+ 规范方程法
|
||||
- 直觉理解
|
||||
- 严格数学推导
|
||||
- 不可逆
|
||||
+ 过拟合
|
||||
- 过拟合概述与产生原因,解决方案
|
||||
- 正则化的损失函数
|
||||
- 规范方程法:一定是可逆阵
|
||||
- 梯度下降法:仍然是凸函数
|
||||
|
||||
## 摘要
|
||||
|
||||
这篇文章主要介绍线性回归问题。任何一种统计学习方法都由三个要素组成,即**模型**,**策略**以及**算法**。所谓模型,就是本文中的**假设函数**(`hypothesis function`);策略或者评价准则,即本文中的**损失函数**;而算法,则是选取最优模型的方法。本文将从这三个方面对线性回归问题进行总结。
|
||||
|
||||
第一部分阐述了线性回归问题的基本概念,给出了它的假设函数以及损失函数的数学表达式,并且指出**平方损失函数**在本质上与**最大似然估计**无异。接下来的两个部分,分别阐述了求解该模型的两种算法,即**梯度下降法**与**规范方程法**,给出了两种方法的数学推导与数学表达式,并就实际应用中可能存在的问题进行讨论。最后,介绍了在统计学习中常见的一个问题——**过拟合**,并且就解决过拟合问题的常见方法——**正则化**(`regularization`)进行了深入的探讨。
|
||||
|
||||
这篇文章的重点在于:平方损失函数与最大似然估计的联系,平方损失函数是凸函数的证明,规范方程法的数学推导,正则化后的平方损失函数仍然是凸函数的证明以及正则化的规范方程一定可逆的证明。
|
||||
|
||||
## 线性回归概述
|
||||
|
||||
> 假设函数与平方损失函数。
|
||||
|
||||
线性回归问题是这样一个问题,对于输入$x$($x$是一个$n$维向量,表示$n$个特征),假设输出$y$是$x$的线性函数。不妨设假设函数为
|
||||
|
||||
$$
|
||||
\hat{y} = h_\theta(x) = \theta_0 + \theta_1x_1 + \cdots + \theta_nx_n
|
||||
$$
|
||||
|
||||
为了简单起见,不妨引入一个$x_0 = 1$,这样
|
||||
|
||||
$$
|
||||
\hat{y} = \Sigma_{i = 0}^n \theta_ix_i = \theta^Tx
|
||||
$$
|
||||
|
||||
这里$\hat{y}$表示对$y$的估计值。因此,线性回归的任务,就是找到一个合适的列向量$\theta$,使得假设函数尽可能反映输入样本的特征。为了衡量假设函数与输入样本的符合程度,需要定义一个新的函数,这里称为损失函数或者代价函数(`cost function`)。
|
||||
|
||||
在线性回归问题中,常用的损失函数为平方损失函数(`square cost function`),定义为$J(\theta)$
|
||||
|
||||
$$
|
||||
J(\theta) = \frac{1}{2m}\Sigma_{i = 1}^m (\hat{y}^{(i)} - y^{(i)})^2 = \frac{1}{2m}\Sigma_{i = 1}^m (h_\theta(x^{(i)}) - y^{(i)})^2
|
||||
$$
|
||||
|
||||
容易注意到,平方损失函数的主体是用最小二乘法给出的,它反映了样本预测值与实际值之间的偏离程度。假设函数越符合样本,则损失函数的值就会越小;反之,假设函数越偏离样本,损失函数的值就会越大。因此可以用损失函数的取值来定量衡量参数$\theta$的好坏,最优的一组$\theta$值,应该恰好使损失函数取到最小值。这样,就将参数优化问题,转化为了求解最值问题。
|
||||
|
||||
> 为什么使用平方损失函数?
|
||||
|
||||
使用平方损失函数并非偶然,而是由最大似然估计推导而出的。下面给出推导过程。
|
||||
|
||||
在线性回归问题中,模型假设实际上是输出$y$乃以$h_\theta(x)$为均值的**正态分布**,即
|
||||
|
||||
$$
|
||||
y|x = h_\theta(x) + \epsilon,\ \epsilon \sim N(0, \sigma^2)
|
||||
$$
|
||||
|
||||
所以有
|
||||
|
||||
$$
|
||||
y|x \sim N(h_\theta(x), \sigma^2)\\\\
|
||||
f(y | x) = \frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{(y - \theta^Tx)^2}{2\sigma^2}}
|
||||
$$
|
||||
|
||||
因此可以写出似然函数$L(\theta)$
|
||||
|
||||
$$
|
||||
L(\theta) = \Pi_{i = 1}^m f(y^{(i)}|x^{(i)}) = (\frac{1}{\sqrt{2\pi}\sigma})^m\cdot e^{-\frac{1}{2\sigma^2}\Sigma_{i = 1}^m (y^{(i)} - \theta^Tx^{(i)})^2}\\\\
|
||||
lnL(\theta) = -mln(\sqrt{2\pi}\sigma) - \frac{1}{2\sigma^2}\Sigma_{i = 1}^m(y^{(i)} - \theta^Tx^{(i)})^2
|
||||
$$
|
||||
|
||||
这样,对似然函数求最大值,就等价于求
|
||||
|
||||
$$
|
||||
\Sigma_{i = 1}^m(y^{(i)} - \theta^Tx^{(i)})^2
|
||||
$$
|
||||
|
||||
的最小值,此时上面的平方损失函数也取到最小值。因此可以看到,平方损失函数实际上与最大似然估计是一脉相承的。
|
||||
|
||||
> 多项式回归。
|
||||
|
||||
实际上,线性回归还可以应用在非线性的场合,比方说有一个非线性的假设函数
|
||||
|
||||
$$
|
||||
h_\theta(x) = \theta_0 + \theta_1x + \theta_2x^2
|
||||
$$
|
||||
|
||||
不妨设$x_1 = x, x_2 = x^2$,这样就可以将上面的假设函数写作
|
||||
|
||||
$$
|
||||
h_\theta(x_1, x_2) = \theta_0 + \theta_1x_1 + \theta_2x_2
|
||||
$$
|
||||
|
||||
这样,就把非线性的假设函数转化为了线性回归问题,从而可以利用线性回归的方法来解决。该问题被称为**多项式回归**问题(`polynomial regression`)。
|
||||
|
||||
理论上来讲,一般的可导函数总是可以通过泰勒展开,写成多项式的形式,因此多项式回归理论上可以解决很多复杂的问题。然而,在实际应用中,当选择的多项式形式过于复杂时,可能会产生过拟合(`overfitting`)的问题,将会在后面阐述。
|
||||
|
||||
## 梯度下降法
|
||||
|
||||
> 梯度下降法的基本表述。
|
||||
|
||||
梯度下降法(`gradient descent`)是求解最优化问题的常见数值方法。它的基本思想是在每次迭代过程中,使得被优化的参数沿着函数下降速度最快的方向(即梯度的方向)移动一个微小的步长,随着迭代的进行,被优化函数的取值会越来越小,直至收敛至极小值点。
|
||||
|
||||
对于任意被优化函数$J(\theta)$,梯度下降法的参数更新公式为
|
||||
|
||||
$$
|
||||
\theta_j := \theta_j - \alpha\frac{\partial}{\partial \theta_j}J(\theta)
|
||||
$$
|
||||
|
||||
这里的$\alpha$称为**学习率**(`learning rate`),它其实就是梯度下降法采用的步长。采用梯度下降法来求解平方损失函数的最小值,则有
|
||||
|
||||
$$
|
||||
\theta_j := \theta_j - \frac{\alpha}{m}\Sigma_{i = 1}^m(h_\theta(x^{(i)}) - y^{(i)})x_j^{(i)}
|
||||
$$
|
||||
|
||||
通过选择合适的迭代次数和学习率,就可以得到使$J(\theta)$最小的$\theta$了。
|
||||
|
||||
### 梯度下降法存在的问题若干
|
||||
|
||||
> 学习率$\alpha$的选择。
|
||||
|
||||
学习率的选择在梯度下降法中至关重要。如果学习率太小,则需要更多次迭代才能找到最优解,需要较长的学习时间;而如果学习率太大,也有可能导致收敛速度慢,甚至可能会发散。这是因为学习率太大时,不再满足全微分方程中微小变化量的条件,因此每次迭代后被优化函数都未必可以取到比上一次迭代更小的值。学习率的选择对$J(\theta)$收敛速度的影响如下图所示:
|
||||
|
||||
<img src = "images/select_alpha.png" align = "center"/>
|
||||
|
||||
可以看到,当$\alpha$很小时(如图中红色曲线所示),损失函数$J(\theta)$收敛速度较慢。随着$\alpha$逐渐增大,曲线逐渐变得陡峭,收敛速度越来越快。可是当$\alpha$很大时($\alpha = 1.3$,图中黑色曲线)收敛速度反而相对此前变慢了;$\alpha$继续增大将导致代价函数发散(图中洋红色曲线)。
|
||||
|
||||
因此,理想的方案是选择这样一个$\alpha$,它应该处于收敛与发散的临界点,一方面保证被优化函数最终收敛,另一方面还要有较快的收敛速度。
|
||||
|
||||
> 局部最优解。
|
||||
|
||||
从上面的讨论中可以看出,梯度下降法最终将收敛到某个局部最优点,而无法保证收敛到全局最优点。实际上,当选择不同的初始值时,使用梯度下降法往往会收敛到不同的极值点,如下图所示:
|
||||
|
||||
<img src = "images/gd_diff_path.jpg" align = "center" />
|
||||
|
||||
因此,使用梯度下降法无法保证得到的结果一定是全局最优解。然而,需要指出,对于线性回归与平方损失函数,则不存在这个问题,因为平方损失函数是**凸函数**。证明如下:
|
||||
|
||||
平方损失函数$J(\theta)$满足
|
||||
|
||||
$$
|
||||
J(\theta) = \frac{1}{2m}\Sigma_{i = 1}^m (h_\theta(x^{(i)}) - y^{(i)})^2
|
||||
$$
|
||||
|
||||
所以
|
||||
|
||||
$$
|
||||
\frac{\partial}{\partial \theta_j}J(\theta) = \frac{1}{m}\Sigma_{i = 1}^m(h_\theta(x^{(i)}) - y^{(i)})x_j^{(i)}\\\\
|
||||
\frac{\partial^2}{\partial \theta_j\partial \theta_k}J(\theta) = \frac{1}{m}\Sigma_{i = 1}^mx_j^{(i)}x_k^{(i)}
|
||||
$$
|
||||
|
||||
因此,可以写出`Hessian`矩阵$H$:
|
||||
|
||||
$$
|
||||
H = \frac{1}{m}\left[
|
||||
\begin{matrix}
|
||||
\Sigma_{i = 1}^mx_0^{(i)^2} & \Sigma_{i = 1}^mx_0^{(i)}x_1^{(i)} & \cdots & \Sigma_{i = 1}^mx_0^{(i)}x_n^{(i)}\\\\
|
||||
\Sigma_{i = 1}^mx_1^{(i)}x_0^{(i)} & \Sigma_{i = 1}^mx_1^{(i)^2} & \cdots & \Sigma_{i = 1}^mx_1^{(i)}x_n^{(i)}\\\\
|
||||
\vdots & \vdots & & \vdots&\\\\
|
||||
\Sigma_{i = 1}^mx_n^{(i)}x_0^{(i)} & \Sigma_{i = 1}^mx_n^{(i)}x_1^{(i)} & \cdots & \Sigma_{i = 1}^mx_n^{(i)^2}\\\\
|
||||
\end{matrix}
|
||||
\right]
|
||||
$$
|
||||
|
||||
容易看出,
|
||||
|
||||
$$
|
||||
H = \frac{1}{m}X^TX
|
||||
$$
|
||||
|
||||
因此$H$显然是一个**半正定矩阵**,根据凸函数判别的充分条件,可以得出平方损失函数为凸函数。
|
||||
|
||||
凸函数具有一系列的优良性质,比如说只有一个极值点,即全局最小值点。因此对凸函数使用梯度下降法总是可以收敛至全局最优点。
|
||||
|
||||
> 多特征的线性回归
|
||||
|
||||
在实际应用中,一个问题往往会有成百上千个特征,甚至更多。这些不同特征的有截然不同的取值范围,如果不同特征取值范围差别很大,则会导致梯度下降法收敛速度慢的问题。
|
||||
|
||||
考虑存在两个特征$x_1, x_2$,它们各自的取值范围分别是$0 < x_1 < 1$,$0 < x_2 < 1000$,两者相差了1000倍。在使用梯度下降法时,参数$\theta_2$的变化量也将大约是$\theta_1$的1000倍,这将导致损失函数等高线图呈现扁平的椭圆状,梯度下降路径相对取值范围更大的特征对应的参数来回波动,从而导致收敛速度缓慢。
|
||||
|
||||
为了解决上面的问题,需要对取值相差比较大的特征进行**特征缩放**(`feature scaling`)与**均值规范**(`mean normalization`),具体的操作是求出每一个特征的均值$\mu$以及范围(最大值减去最小值),随后对每一个特征点进行以下更新
|
||||
|
||||
$$
|
||||
x_j^{(i)} := \frac{x_j^{(i)} - \mu^{(i)}}{range^{(i)}}
|
||||
$$
|
||||
|
||||
在实际应用中,也可以用**标准差**(`standard deviation`)来代替范围进行更新。这样更新后的特征取值范围都在$[-1, 1]$之间,绘出的等高线图接近于(高维)球形,收敛速度就要快多了。
|
||||
|
||||
## 规范方程
|
||||
|
||||
> 规范方程的直觉理解
|
||||
|
||||
前面提到假设函数的表达式为
|
||||
|
||||
$$
|
||||
h_\theta(x) = \theta^Tx = x^T\theta
|
||||
$$
|
||||
|
||||
设样本容量为$m$,则输入$X$以及对应的输出$Y$都可以写成矩阵的形式
|
||||
|
||||
$$
|
||||
X = \left[
|
||||
\begin{matrix}
|
||||
x^{(1)^T}\\\\
|
||||
x^{(2)^T}\\\\
|
||||
\vdots\\\\
|
||||
x^{(m)^T}
|
||||
\end{matrix}
|
||||
\right],\ \
|
||||
Y = \left[
|
||||
\begin{matrix}
|
||||
y^{(1)}\\\\
|
||||
y^{(2)}\\\\
|
||||
\vdots\\\\
|
||||
y^{(m)}
|
||||
\end{matrix}
|
||||
\right],\ \
|
||||
\theta = \left[
|
||||
\begin{matrix}
|
||||
\theta_0\\\\
|
||||
\theta_1\\\\
|
||||
\vdots\\\\
|
||||
\theta_n
|
||||
\end{matrix}
|
||||
\right]
|
||||
$$
|
||||
|
||||
这样,就可以把假设函数也写成矩阵形式
|
||||
|
||||
$$
|
||||
Y = X\theta
|
||||
$$
|
||||
|
||||
为了得到$\theta$,只需求解该矩阵方程,为此将等式的两边同时乘以$X^T$
|
||||
|
||||
$$
|
||||
X^TY = X^TX\theta
|
||||
$$
|
||||
|
||||
所以
|
||||
|
||||
$$
|
||||
\theta = (X^TX)^{-1}X^TY
|
||||
$$
|
||||
|
||||
这就是规范方程法的数学表述。以下就这个结论给出严格推导。
|
||||
|
||||
> 规范方程法的推导
|
||||
|
||||
平方损失函数(cost function)$J(\theta)$可以写作
|
||||
|
||||
$$
|
||||
J(\theta) = \frac{1}{2m}\Sigma_{i = 1}^m[h_\theta(x^{(i)}) - y^{(i)}]^2
|
||||
$$
|
||||
|
||||
参数$\theta$应该是使$J(\theta)$最小的一组列向量。由多元函数极值可知,$\theta$应该满足:
|
||||
|
||||
$$
|
||||
\frac{\partial}{\partial \theta_j}J(\theta) = \frac{1}{m}\Sigma_{i = 1}^m[h_\theta(x^{(i)}) - y^{(i)}]x_j^{(i)} = 0
|
||||
$$
|
||||
|
||||
解上述方程即可以得到$\theta$的解析解。
|
||||
|
||||
为了简单起见,下面用矩阵形式来对$J(\theta)$进行表述。所以$J(\theta)$可以写作
|
||||
|
||||
$$
|
||||
J(\theta) = \frac{1}{2m}(X\theta - Y)^T(X\theta - Y) = \frac{1}{2m}(\theta^TX^TX\theta - 2Y^TX\theta + Y^TY)
|
||||
$$
|
||||
|
||||
对$\theta$求导,所以
|
||||
|
||||
$$
|
||||
\frac{d}{d\theta}J(\theta) = \frac{1}{2m}(2X^TX\theta - 2X^TY) = 0
|
||||
$$
|
||||
|
||||
从而可得
|
||||
|
||||
$$
|
||||
\theta = (X^TX)^{-1}X^TY
|
||||
$$
|
||||
|
||||
该结论与上面的直觉理解完全一致。
|
||||
|
||||
> 规范方程法存在的问题
|
||||
|
||||
需要注意的是,在实际应用中,$X^TX$往往是不可逆的,比如当特征的数量多于样本容量时。因此,往往是通过对$X^TX$求伪逆,来代替$(X^TX)^{-1}$。
|
||||
|
||||
> 梯度下降法与规范方程法的比较
|
||||
|
||||
在通常情况下,当样本的特征比较少时,比如特征数少于10000时,选择规范方程法会有比较好的效果;而当样本数量太多,则应该选择梯度下降法来对模型进行求解。
|
||||
|
||||
这是因为,在规范方程法中,涉及到矩阵的求逆运算,其时间复杂度是$O(n^3)$,因此当模型的特征太多时,该逆矩阵的计算会消耗大量的时间。反之,即使特征数量非常多,梯度下降法也仍然具有较好的性能。
|
||||
|
||||
## 过拟合
|
||||
|
||||
> 过拟合概述
|
||||
|
||||
过拟合是统计学习中非常常见的一个问题,它是指模型的选择过于复杂,因而可以在训练集上得到很好的训练效果,使得损失函数接近于零,可是却难以将该模型**泛化**(`generalize`)到其他应用场合,比如说在一个新的测试集上预测准确率就会非常低下。**欠拟合**,正常拟合,以及过拟合的关系如下图所示
|
||||
|
||||

|
||||
|
||||
可以看到,第一张图中的模型选择过于简单,用线性函数去拟合一个多项式回归问题,发生了欠拟合;第三张图的模型则过于复杂,用了一个非常高阶的多项式来拟合一个低阶问题,发生了过拟合。
|
||||
|
||||
关于过拟合,我想到这样一个直观的理解:训练模型的过程就好比一个人复习考试的过程,一种复习方案是深入理解题目背后的原理和知识点,另一种复习方案则是把每个题目都记忆下来。显然后者在训练集上会有出色的表现,如果记忆力足够好,甚至可以达到100%的正确率,可是在实际考试过程中,遇到的都是他从未记忆过的题目,此时就束手无策了。第二种复习方案就是这里的过拟合。
|
||||
|
||||
过拟合会发生,主要是因为模型选择过于复杂,选择的特征太多。因此,一种解决过拟合的方案,就是手动剔除一些不重要的特征,简化模型,或者可以使用**模型选择算法**(`model selection algorithm`),后者将在后面的文章中提到。另一种更加常见的方案,就是下面要重点阐述的正则化。
|
||||
|
||||
> 正则化的损失函数
|
||||
|
||||
考虑这样一种情况,当前模型的假设函数为
|
||||
|
||||
$$
|
||||
h_\theta(x) = \theta_0 + \theta_1x + \theta_2x^2 + \theta_3x^3 + \theta_4x^4
|
||||
$$
|
||||
|
||||
为了避免发生过拟合,我可以限制$\theta_3, \theta_4$的大小,使得高阶项具有相对更小的权重。具体的方案是在损失函数中对$\theta_3, \theta_4$添加**罚项**(`penalty`),比如
|
||||
|
||||
$$
|
||||
J(\theta) = \frac{1}{2m}\Sigma_{i = 1}^m[h_\theta(x^{(i)}) - y^{(i)}]^2 + 1000\theta_3^2 + 1000\theta_4^2
|
||||
$$
|
||||
|
||||
这样,为了使$J(\theta)$取到最小值,$\theta_3, \theta_4$的取值必然相对较小,因此在一定程度上简化了模型。
|
||||
|
||||
在实际应用中,很难知道哪一些特征会导致模型过拟合,因此常见的方案是对每一个$\theta_j$,都添加一个上述的罚项,也称为**正则化项**(`regularized term`)。这样,正则化的损失函数就有以下的形式:
|
||||
|
||||
$$
|
||||
J(\theta) = \frac{1}{2m}[\Sigma_{i = 1}^m(h_\theta(x^{(i)}) - y^{(i)})^2 + \lambda\Sigma_{i = 1}^n\theta_i^2]
|
||||
$$
|
||||
|
||||
需要注意的是,这里并没有对$\theta_0$引入正则化项,老师说这是一种约定俗成,并没有特别的原因。使用梯度下降法或者规范方程法,就可以求得在正则化下的最优参数$\theta$了。
|
||||
|
||||
> 引入正则化项的规范方程推导
|
||||
|
||||
为了令引入正则化项的$J(\theta)$取到最小值,同样可以利用多元函数极值,令
|
||||
|
||||
$$
|
||||
\frac{\partial}{\partial \theta_j}J(\theta) = 0
|
||||
$$
|
||||
|
||||
逐个求解出$\theta_j$,就可以得到最优的一组$\theta$,在此不再赘述。
|
||||
|
||||
为了简化问题,仍然用矩阵形式表示$J(\theta)$,所以
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
J(\theta) = &\frac{1}{2m}[(X\theta - Y)^T(X\theta - Y) + \lambda(I\theta)^TI\theta]\\\\
|
||||
= &\frac{1}{2m}(\theta^TX^TX\theta - 2Y^TX\theta + Y^TY + \lambda(I\theta)^TI\theta]
|
||||
\end{aligned}
|
||||
$$
|
||||
其中
|
||||
|
||||
$$
|
||||
I = \left[
|
||||
\begin{matrix}
|
||||
0 & 0 & \cdots & 0\\\\
|
||||
0 & 1 & \cdots & 0\\\\
|
||||
\vdots & \vdots & & \vdots\\\\
|
||||
0 & 0 & \cdots & 1
|
||||
\end{matrix}
|
||||
\right].
|
||||
$$
|
||||
|
||||
所以
|
||||
|
||||
$$
|
||||
\frac{\partial}{\partial \theta}J(\theta) = \frac{1}{2m}(2X^TX\theta - 2X^TY + 2\lambda I\theta) = 0
|
||||
$$
|
||||
|
||||
从而可以解出
|
||||
|
||||
$$
|
||||
\theta = (X^TX + \lambda I)^{-1}X^TY = (X^TX + \lambda \left[
|
||||
\begin{matrix}
|
||||
0 & 0 & \cdots & 0\\\\
|
||||
0 & 1 & \cdots & 0\\\\
|
||||
\vdots & \vdots & & \vdots\\\\
|
||||
0 & 0 & \cdots & 1
|
||||
\end{matrix}
|
||||
\right])^{-1}X^TY
|
||||
$$
|
||||
|
||||
需要说明的是,正则化项的引入不仅解决了过拟合的问题,还使得新的矩阵
|
||||
$$
|
||||
X^TX + \lambda I
|
||||
$$
|
||||
|
||||
一定是可逆矩阵,而此前的
|
||||
|
||||
$$
|
||||
X^TX
|
||||
$$
|
||||
|
||||
则不能保证它的可逆性。下面给出证明。
|
||||
|
||||
前面已经指出
|
||||
|
||||
$$
|
||||
X = \left[
|
||||
\begin{matrix}
|
||||
x^{(1)^T}\\\\
|
||||
x^{(2)^T}\\\\
|
||||
\vdots\\\\
|
||||
x^{(m)^T}
|
||||
\end{matrix}
|
||||
\right] = \left[
|
||||
\begin{matrix}
|
||||
x_0^{(1)} & x_1^{(1)} & \cdots & x_n^{(1)}\\\\
|
||||
x_0^{(2)} & x_1^{(2)} & \cdots & x_n^{(2)}\\\\
|
||||
\vdots & \vdots & & \vdots\\\\
|
||||
x_0^{(m)} & x_1^{(m)} & \cdots & x_n^{(m)}\\\\
|
||||
\end{matrix}
|
||||
\right]
|
||||
$$
|
||||
|
||||
所以
|
||||
|
||||
$$
|
||||
X^T = \left[
|
||||
\begin{matrix}
|
||||
x_0^{(1)} & x_0^{(2)} & \cdots & x_0^{(m)}\\\\
|
||||
x_1^{(1)} & x_1^{(2)} & \cdots & x_1^{(m)}\\\\
|
||||
\vdots & \vdots & & \vdots\\\\
|
||||
x_n^{(1)} & x_n^{(2)} & \cdots & x_n^{(m)}\\\\
|
||||
\end{matrix}
|
||||
\right]
|
||||
$$
|
||||
|
||||
容易注意到,$X^TX$的对角线元素一定大于或者等于零。且对角线上第一个元素为
|
||||
|
||||
$$
|
||||
a_{11} = \Sigma_{i = 1}^mx_0^{(i)}\cdot x_0^{(i)} = m
|
||||
$$
|
||||
|
||||
因此$X^T + \lambda I$的对角线元素$a_{ii}$一定大于$0$,对于任意的$\lambda > 0$成立。所以
|
||||
|
||||
$$
|
||||
\left| X^T + \lambda I \right| = \Pi_{i = 1}^{n + 1}a_{ii} > 0
|
||||
$$
|
||||
|
||||
因此$X^T + \lambda I$一定是一个可逆矩阵。实际上,还可以得到更进一步的结论,即$X^T + \lambda I$一定是一个正定矩阵。这是因为$X^TX$和$I$都是半正定矩阵,即对于任意的非零向量$\eta$,都有
|
||||
|
||||
$$
|
||||
\eta^TX^TX\eta \ge 0,\ \eta^TI\eta \ge 0
|
||||
$$
|
||||
|
||||
因此两者之和也必定是一个半正定矩阵。由于此前已经得到$X^T + \lambda I$可逆,故不存在为零的特征值,所以所有特征值都必定大于零,因此$X^T + \lambda I$是一个正定矩阵。
|
||||
|
||||
> 引入正则化项的梯度下降法
|
||||
|
||||
关于梯度下降法自然不必多说,这里主要证明,引入正则化项后的平方损失函数仍然是凸函数,因此使用梯度下降法仍然可以收敛到全局最优点。
|
||||
|
||||
再次给出$J(\theta)$的表达式
|
||||
|
||||
$$
|
||||
J(\theta) = \frac{1}{2m}[\Sigma_{i = 1}^m(h_\theta(x^{(i)}) - y^{(i)})^2 + \lambda\Sigma_{i = 1}^n\theta_i^2]
|
||||
$$
|
||||
|
||||
所以有
|
||||
|
||||
$$
|
||||
\frac{\partial}{\partial \theta_0}J(\theta) = \frac{1}{m}\Sigma_{i = 1}^m(h_\theta(x^{(i)}) - y^{(i)})x_0^{(i)}\\\\
|
||||
\frac{\partial}{\partial \theta_j}J(\theta) = \frac{1}{m}\Sigma_{i = 1}^m(h_\theta(x^{(i)}) - y^{(i)})x_j^{(i)} + \frac{1}{m}\theta_j\ (j \ne 0)
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial^2}{\partial \theta_0^2}J(\theta) = \frac{1}{m}\Sigma_{i = 1}^mx_0^{(i)}x_0^{(i)}\\\\
|
||||
\frac{\partial^2}{\partial \theta_j^2}J(\theta) = \frac{1}{m}\Sigma_{i = 1}^mx_j^{(i)}x_j^{(i)} + \frac{\lambda}{m}\ (j \ne 0)\\\\
|
||||
\frac{\partial^2}{\partial \theta_j\partial \theta_k}J(\theta) = \frac{1}{m}\Sigma_{i = 1}^mx_j^{(i)}x_k^{(i)} \ (j \ne k)
|
||||
$$
|
||||
|
||||
因此,可以写出`Hessian`矩阵
|
||||
|
||||
$$
|
||||
H = \frac{1}{m}\left[
|
||||
\begin{matrix}
|
||||
\Sigma_{i = 1}^mx_0^{(i)^2} & \Sigma_{i = 1}^mx_0^{(i)}x_1^{(i)} & \cdots & \Sigma_{i = 1}^mx_0^{(i)}x_n^{(i)}\\\\
|
||||
\Sigma_{i = 1}^mx_1^{(i)}x_0^{(i)} & \Sigma_{i = 1}^mx_1^{(i)^2} + \lambda & \cdots & \Sigma_{i = 1}^mx_1^{(i)}x_n^{(i)} \\\\
|
||||
\vdots & \vdots & & \vdots&\\\\
|
||||
\Sigma_{i = 1}^mx_n^{(i)}x_0^{(i)} & \Sigma_{i = 1}^mx_n^{(i)}x_1^{(i)} & \cdots & \Sigma_{i = 1}^mx_n^{(i)^2} + \lambda\\\\
|
||||
\end{matrix}
|
||||
\right]
|
||||
$$
|
||||
|
||||
容易看出,
|
||||
|
||||
$$
|
||||
H = \frac{1}{m}(X^TX + \lambda I)
|
||||
$$
|
||||
|
||||
上面已经证明了,该矩阵是一个正定矩阵。根据凸函数的充分条件,可以得到正则化后的平方损失函数仍然是一个凸函数。
|
||||
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,292 +0,0 @@
|
||||
逻辑回归问题总结
|
||||
==============
|
||||
|
||||
## 目录
|
||||
+ 摘要
|
||||
+ 逻辑回归概述
|
||||
- 逻辑回归的假设函数
|
||||
+ 假设函数的意义
|
||||
+ 决策边界
|
||||
- 损失函数
|
||||
+ 为什么对数似然函数
|
||||
+ 对数似然函数是凸函数
|
||||
- 多分类问题
|
||||
+ 过拟合
|
||||
- 正则化的损失函数
|
||||
|
||||
## 逻辑回归概述
|
||||
|
||||
在 [线性回归总结](../linear_regression/linear_regression.md) 中,简单介绍了线性回归问题,这里将要阐述的逻辑回归,却并不是回归问题,而是一个分类问题(`classification problem`)。分类问题与回归问题的主要界限,在于分类问题的输出是离散值,而回归问题的输出是连续的。根据分类问题输出取值数量的不同,又可以分为**二分类问题**和**多分类问题**,这里主要阐述二分类问题。
|
||||
|
||||
二分类问题的输出一般为0或者1,表示某一特定的事件不发生或者发生,比如辅助诊断系统判断患者患病或者不患病,邮件分类系统判断邮件是否是垃圾邮件,都是典型的二分类问题。
|
||||
|
||||
为了训练二分类问题,一种直觉上的方法是直接套用此前的线性回归模型,这样就可以得到模型的假设函数为
|
||||
|
||||
$$
|
||||
h_\theta(x) = \theta^Tx
|
||||
$$
|
||||
|
||||
仍然使用此前的平方损失函数以及梯度下降法(或者规范方程法)来对模型进行求解,对于某个简单的数据集,可以得到下面的结果
|
||||
|
||||

|
||||
|
||||
此时$h_\theta(x)$的输出值仍然是连续值,而二分类问题要求模型的输出是0或者1,因此需要对假设函数的输出进行进一步处理。这里可以简单地令$\hat{y} = 1$当且仅当$h_\theta(x) >= 0.5$,令$\hat{y} = 0$当且仅当$h_\theta(x) < 0.5$。可以看到,采用这种策略时,对上图的数据具有很高的预测正确率。
|
||||
|
||||
然而这种方法却也有局限性,考虑在训练集中增加一个样本点$X = 80, y = 1$,此时线性回归模型的结果如下图所示:
|
||||
|
||||

|
||||
|
||||
可以看到,此时回归直线相对此前向右偏移,模型对训练集的预测能力下降了不少。可以想象,如果训练集中还有更多这样的极端数据,回归的直线将继续向右偏移,模型的预测能力将继续下降。
|
||||
|
||||
此外,线性回归模型的假设函数,其值域是$(-\infty, +\infty)$,而分类问题的输出$y$无非是0或者1而已,当$h_\theta(x) > 1$或者$h_\theta(x) < 0$时不具有意义。可见,简单套用线性回归的方法来解决分类问题是行不通的。
|
||||
|
||||
### 逻辑回归的假设函数
|
||||
|
||||
可以做一些简单的修改,使得假设函数的值域限制在$[0, 1]$。这里引入**sigmoid函数**,它的表达式$g(z)$满足
|
||||
|
||||
$$
|
||||
g(z) = \frac{1}{1 + e^{-z}}
|
||||
$$
|
||||
|
||||
可以看出,它的取值范围恰好在$(0, 1)$之间,实际上,它的图像如下所示:
|
||||
|
||||

|
||||
|
||||
因此,可以把 sigmoid 函数作用到此前的线性回归模型上,就得到了新的假设函数
|
||||
|
||||
$$
|
||||
h_\theta(x) = sigmoid(\theta^Tx)
|
||||
$$
|
||||
|
||||
此时,它的函数值,就可以被理解成在输入为$x$的条件下,$y = 1$的概率,即
|
||||
|
||||
$$
|
||||
P(y = 1| x) = sigmoid(\theta x)
|
||||
$$
|
||||
|
||||
> 决策边界
|
||||
|
||||
以下对 sigmoid 函数进行进一步的讨论。前面已经指出,假设函数的返回值,表示的是预测样本为正(`positive`)的概率。一般地,当$h_\theta(x) >= 0.5$时,预测$\hat{y} = 1$;当$h_\theta(y) < 0.5$时,预测$\hat{y} = 0$。这样,$h_\theta(x) = 0.5$就成为正负样本的边界,称为**决策边界**(`decision boundary`)。
|
||||
|
||||
由
|
||||
|
||||
$$
|
||||
h_\theta(x) = sigmoid(\theta^Tx) = 0.5
|
||||
$$
|
||||
|
||||
根据 sigmoid 函数的图像,恰好可以得到
|
||||
|
||||
$$
|
||||
\theta^Tx = \theta_0x_0 + \theta_1x_1 + \theta_2x_2 + \cdots + \theta_nx_n = 0
|
||||
$$
|
||||
|
||||
容易看出,这是一个(高维)平面的方程,预测为正负的样本,分别分布在该平面的两侧。因此,逻辑回归的假设函数,本质上就是找到这样一个高维平面对样本点进行划分,达到尽可能高的划分正确率。
|
||||
|
||||
利用**多项式回归**,可以得到更加复杂的决策边界。比如令
|
||||
|
||||
$$
|
||||
h_\theta(x) = sigmoid(\theta_0 + \theta_1x_1 + \theta_2x_2 + \theta_3x_1^2 + \theta_4x_2^2)
|
||||
$$
|
||||
|
||||
则得到的决策边界为
|
||||
|
||||
$$
|
||||
\theta_0 + \theta_1x_1 + \theta_2x_2 + \theta_3x_1^2 + \theta_4x_2^2 = 0
|
||||
$$
|
||||
|
||||
容易看出,这是一个圆锥曲线的方程。因此,通过构造更高阶的多项式,可以得到相当复杂的决策边界,从而将逻辑回归应用到更加复杂的分类问题当中。
|
||||
|
||||
### 逻辑回归的损失函数
|
||||
|
||||
在线性回归当中,是使用平方损失函数来作为损失函数,逻辑回归也可以沿用这种方式,即定义$J(\theta)$满足
|
||||
|
||||
$$
|
||||
J(\theta) = \frac{1}{2m}\Sigma_{i = 1}^m(h_\theta(x^{(i)}) - y^{(i)})^2
|
||||
$$
|
||||
|
||||
为了找到一组最优的$\theta$,应该使平方损失函数具有最小值,这里仍然沿用**梯度下降法**来求解。然而,在逻辑回归中,上面的平方损失函数却并非一个凸函数,使用梯度下降法无法确保收敛到全局最优点,等以后我变强了可以考虑给个证明。
|
||||
|
||||
因此,实际上并不是使用平方损失函数,而是**对数损失函数**,来作为逻辑回归的损失函数。此时,单个样本的损失函数$cost(h_\theta(x), y)$满足
|
||||
|
||||
$$
|
||||
cost(h_\theta(x), y) =
|
||||
\left\\{
|
||||
\begin{aligned}
|
||||
-log(h_\theta(x))&, y = 1\\\\
|
||||
-log(1 - h_\theta(x))&, y = 0
|
||||
\end{aligned}
|
||||
\right.
|
||||
$$
|
||||
|
||||
可以分别画出$y = 0$与$y = 1$的损失函数的图像,以获得一个比较直观的理解。容易看到,当$y = 1$时,对输出的预测值$h_\theta(x)$越接近一,则损失函数的值越小;预测值越接近零,则损失函数的值越大。并且当$h_\theta(x) = 1$时,$cost(h_\theta(x), 1) = 0$,当$h_\theta(x) = 0$时,$cost(h_\theta(x), 1) = +\infty$。该性质是容易理解的,即预测值与真值越接近,则相应的损失就越小。$y = 0$时也有类似的性质。
|
||||
|
||||
可以用一种更加简单的方法来表示$cost(h_\theta(x), y)$,即
|
||||
|
||||
$$
|
||||
cost(h_\theta(x), y) = -ylog(h_\theta(x)) - (1-y)log(1 - h_\theta(x))
|
||||
$$
|
||||
|
||||
因此,整个样本的损失函数为
|
||||
|
||||
$$
|
||||
J(\theta) = \frac{1}{m}\Sigma_{i = 1}^mcost(h_\theta(x^{(i)}), y^{(i)}) = -\frac{1}{m}[\Sigma_{i = 1}^my^{(i)}log(h_\theta(x^{(i)})) + (1-y^{(i)})log(1 - h_\theta(x^{(i)}))]
|
||||
$$
|
||||
|
||||
> 为什么使用对数似然函数?
|
||||
|
||||
使用对数似然函数并非偶然,而是由**最大似然估计**推导而出的。下面给出推导过程:
|
||||
|
||||
假设函数$h_\theta(x)$满足
|
||||
|
||||
$$
|
||||
h_\theta(x) = sigmoid(\theta^T \cdot x)
|
||||
$$
|
||||
|
||||
它的意义是在输入为$x$的条件下,输出$y = 1$的概率,即
|
||||
|
||||
$$
|
||||
P(y = 1| x) = sigmoid(\theta^T x)
|
||||
$$
|
||||
|
||||
因此$y | x$服从这样一个两点分布,其中
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
&P(y = 1| x) = sigmoid(\theta^T x)\\\\
|
||||
&P(y = 0| x) = 1 - sigmoid(\theta^T x)
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
设样本容量为$m$,其中$y = 1$的样本有$k$个,所以可以写出似然函数
|
||||
|
||||
$$
|
||||
L(\theta) = \Pi_{y^{(i)} = 1}sigmoid(\theta x^{(i)}) \cdot \Pi_{y^{(j) } = 0}(1 - sigmoid(\theta x^{(j)}))
|
||||
$$
|
||||
|
||||
对似然函数进行一些化简
|
||||
|
||||
$$
|
||||
L(\theta) = \Pi_{i = 1}^m \{[h_\theta(x^{(i)})]^{y^{(i)}} \cdot [(1 - h_\theta(x^{(i)}))]^{1-y^{(i)}}\}
|
||||
$$
|
||||
|
||||
从而可以得到
|
||||
|
||||
$$
|
||||
lnL(\theta) = \Sigma_{i = 1}^m [y^{(i)}lnh_\theta(x^{(i)}) + (1 - y^{(i)})ln(1 - h_\theta(x^{(i)})]
|
||||
$$
|
||||
|
||||
这样,求似然函数的最大值,就等价于求
|
||||
|
||||
$$
|
||||
J(\theta) = -\frac{1}{m}[\Sigma_{i = 1}^my^{(i)}ln(h_\theta(x^{(i)})) + (1-y^{(i)})ln(1 - h_\theta(x^{(i)}))]
|
||||
$$
|
||||
|
||||
的最小值。可见,对数损失函数,实际上与最大似然估计是一脉相承的。
|
||||
|
||||
> 对数损失函数是凸函数
|
||||
|
||||
相较于平方损失函数,对数似然函数还具有一个优良性质,即它是一个凸函数。证明如下:
|
||||
|
||||
再次给出$J(\theta)$的表达式:
|
||||
|
||||
$$
|
||||
J(\theta) = -\frac{1}{m}[\Sigma_{i = 1}^my^{(i)}log(h_\theta(x^{(i)})) + (1-y^{(i)})log(1 - h_\theta(x^{(i)}))]
|
||||
$$
|
||||
|
||||
所以
|
||||
|
||||
$$
|
||||
\frac{\partial}{\partial \theta_j}J(\theta) = \frac{1}{m}\Sigma_{i = 1}^m(h_\theta(x^{(i)}) - y^{(i)})x_j^{(i)}
|
||||
$$
|
||||
|
||||
这里应该看到,对数损失函数以及平方损失函数的一阶偏导的形式是完全相同的(尽管其中的$h_\theta(x)$不同),据说这并不是巧合,而是存在一些深层次的原因,有机会我会回来说明一下。
|
||||
|
||||
因此可以给出二阶偏导的表达式
|
||||
|
||||
$$
|
||||
\frac{\partial^2}{\partial \theta_j \partial \theta_k}J(\theta) = \frac{1}{m}\Sigma_{i = 1}^mx_j^{(i)}\frac{e^{-z}}{(1 + e^{-z})^2}x_k^{(i)},\ z = \theta^Tx^{(i)}
|
||||
$$
|
||||
|
||||
可以看出,$\frac{e^{-z}}{(1 + e^{-z})^2}$一定是大于零的。不妨设$g(x)$为
|
||||
|
||||
$$
|
||||
g(x) = \sqrt{\frac{e^{-\theta^Tx}}{(1 + e^{-\theta^Tx})^2}}
|
||||
$$
|
||||
|
||||
构造矩阵$G$为
|
||||
|
||||
$$
|
||||
G = \left[
|
||||
\begin{matrix}
|
||||
g(x^{(1)}) & & &\\\\
|
||||
& g(x^{(2)}) & &\\\\
|
||||
& & \ddots & \\\\
|
||||
& & & g(x^{(m)})
|
||||
\end{matrix}
|
||||
\right]
|
||||
$$
|
||||
|
||||
所以对数损失函数的 Hessian 矩阵为
|
||||
|
||||
$$
|
||||
H = \frac{1}{m}\left[
|
||||
\begin{matrix}
|
||||
\frac{\partial^2}{\partial \theta_0^2}J(\theta) & \frac{\partial^2}{\partial \theta_0 \partial \theta_1}J(\theta) & \cdots & \frac{\partial^2}{\partial \theta_0 \partial \theta_n}J(\theta)\\\\
|
||||
\frac{\partial^2}{\partial \theta_1 \partial \theta_0}J(\theta) & \frac{\partial^2}{\partial \theta_1^2}J(\theta)
|
||||
& \cdots & \frac{\partial^2}{\partial \theta_1 \partial \theta_n}J(\theta)\\\\
|
||||
\vdots & \vdots & & \vdots\\\\
|
||||
\frac{\partial^2}{\partial \theta_n \partial \theta_0}J(\theta) & \frac{\partial^2}{\partial \theta_n \partial \theta_1}J(\theta) & \cdots & \frac{\partial^2}{\partial \theta_n^2}J(\theta)
|
||||
\end{matrix}
|
||||
\right]
|
||||
$$
|
||||
|
||||
容易看出(?
|
||||
|
||||
$$
|
||||
H = \frac{1}{m}(GX)^TGX
|
||||
$$
|
||||
|
||||
因此,Hessian 矩阵是一个半正定矩阵,从而对数损失函数确实是一个凸函数,得证。同理可得,规范化后的对数损失函数也是一个凸函数,这里不再赘述。这样,利用梯度下降法求解对数损失函数的最小值,一定可以收敛到全局最优点。
|
||||
|
||||
### 多分类问题
|
||||
|
||||
顾名思义,多分类问题中预测的结果可以有多个(多于两个)取值,比如给定一个图片集,将每张图片划分为行人,车辆,道路,树木四个类别。为了求解多分类问题,可以简单地套用二分类问题的方法。
|
||||
|
||||
具体说来,假设多分类问题中一共有$k(k \ge 3)$个类别,因此预测值$y$有$k$个不同的取值。套用二分类问题的方法是,对于每一个类别$c$,定义输出为$c$时$y = 1$,输出不为$c$时$y = 0$,这样就把问题转化为了一个二分类问题。可见,对于任一个类别,都需要训练一个二分类器,最终一共需要训练$k$个二分类器,即
|
||||
|
||||
$$
|
||||
h_\theta^{(i)}(x) = P(y = i | x)\ (i = 1, 2, 3, ..., k)
|
||||
$$
|
||||
|
||||
对模型进行预测时,分别使用$k$个二分类器对模型进行预测,可以得到$k$个概率,分别表示在当前输入的条件下,输出为某个类别的可能性。最后,简单选择可能性最大的一个类别,作为多分类问题的输出,即
|
||||
|
||||
$$
|
||||
max_i h_\theta^{(i)}(x)
|
||||
$$
|
||||
|
||||
## 过拟合
|
||||
|
||||
在此前的 [线性回归总结](../linear_regression/linear_regression.md) 中,已经对过拟合问题进行了一些探讨,这里主要是结合实例,直观地说明过拟合问题及其解决方案。
|
||||
|
||||
为了解决过拟合,仍然可以对此前的对数损失函数添加**正则化项**,正则化后的损失函数$J(\theta)$
|
||||
|
||||
$$
|
||||
J(\theta) = -\frac{1}{m}[\Sigma_{i = 1}^my^{(i)}log(h_\theta(x^{(i)})) + (1-y^{(i)})log(1 - h_\theta(x^{(i)}))] + \frac{\lambda}{2m}\Sigma_{j = 1}^n\theta_j^2
|
||||
$$
|
||||
|
||||
下面以一个具体实例,直观地展示过拟合现象以及正则化对过拟合的影响。这个实例是一个二类分类问题,输入$x$是一个二维向量,即具有两个特征$x_1, x_2$。首先对原始数据进行可视化,如下图所示:
|
||||
|
||||

|
||||
|
||||
其中用圆圈标识$y = 0$的实例,用十字标识$y = 1$的实例,容易看出,原始数据并不服从线性分布,因此考虑添加高次项,利用多项式回归对该模型进行拟合。这里是添加了$x_1, x_2$的所有二次项与三次项,再加上$x_0$,处理后的输入具有28个特征。
|
||||
|
||||
之后,我手动将这些数据划分成了训练集和测试集,其中测试集占所有数据的20%。然后分别使用未正则化的损失函数与正则化的损失函数($\lambda = 1$),利用梯度下降法对模型进行训练。结果如下:
|
||||
|
||||
<table><tr>
|
||||
<td><img src="images/overfit.png" width="90%" height="90%" /></td>
|
||||
<td><img src="images/justfit.png" width="90%" height="90%" /></td>
|
||||
</tr></table>
|
||||
|
||||
图中黑色的圆圈或者十字,是表示训练集的实例;而红色边界的圆圈或者十字,表示的是测试集实例。可以看到,未经过正则化的损失函数,训练出来的模型决策边界相对复杂,具有许多高次项,同时对于训练集有更高的预测准确率(87.21%);而正则化后的损失函数,利用梯度下降法训练出的决策边界则要简单很多,比较规则,对训练集的准确率也要低一些(80.23%)。
|
||||
|
||||
尽管如此,未正则化的模型,对于测试集数据的预测正确率则要低了不少(81.25%),而正则化的模型,则对测试集有更高的预测准确率(84.375%)。这说明没有正则化虽然可以获得更高的训练集预测准确率,却难以将训练得到的模型**泛化**到其他输入数据当中,因此发生了过拟合。而引入正则化则可以使训练的模型具有更好的**泛化能力**。
|
||||
@@ -1,54 +0,0 @@
|
||||
Handscripts when studing Machine Learning
|
||||
=========================================
|
||||
|
||||
> 什么是机器学习?
|
||||
|
||||
注意三个点,即E, T, P。
|
||||
|
||||
> 监督学习与无监督学习之间的区别?
|
||||
|
||||
监督学习是指对于输入的数据,它所对应的输出是已知的。监督学习可以分为两类,即回归问题与分类问题,它们的区别在于输出是否是连续的。具体的例子有房价预测问题(回归问题),判断肿瘤是否是良性(分类问题)。
|
||||
|
||||
无监督学习的输入数据之间没有任何区别,每个输入数据都是等价的,并没有事先表明它的状态或者分类信息(比如房价或者恶性肿瘤),而是由机器来分辨不同数据的属性。典型的例子有`聚类问题`(clustering)以及鸡尾酒宴算法。
|
||||
|
||||
> 关于深度学习算法的一些思考。
|
||||
|
||||
人工神经元算法的设计乃是`线性内核`与`非线性激活`的叠加。根据`线性内核`的不同,可以分为`DNN`,`CNN`,`RNN`,它们分别适用于不同的场景。但是这种建模方法显然是不准确的,片面的,因为实际中的神经元对于各种场合的问题都可以很好的适用。这样,应该存在一种更好的方式来模拟神经元。
|
||||
|
||||
人工神经网络的精髓都在于对大脑中的神经元进行模拟。但是我在想神经元并非一定是解决问题的最高效的方法,虽然神经元经过了几十亿年的进化与自然选择,但它未必是解决现实问题的最优解,可能只是一个局部最优而已,alphaGo的例子就说明了这一点——人类数千年形成的围棋算法实际上只是局部最优解。
|
||||
|
||||
另一方面,让计算机模拟人脑也未必就是最好的方法。因此我在想,有没有可能跳出现有神经元的桎梏,开创出一个更优化的算法,这样说不定还可以反过来对人类的神经元进行改造。
|
||||
|
||||
> 梯度下降法存在的问题。
|
||||
|
||||
首先是学习率(learning rate)的选择。如果$\alpha$太小,则需要多次迭代才能找到局部最优解,需要较长的学习时间;而如果$\alpha$太大,则可能直越过最低点,导致无法收敛,甚至发散。
|
||||
|
||||
因此$\alpha$的选择,最好是选择可以使代价函数收敛的最大的$\alpha$,为了找到这样一个$\alpha$,可以作出代价函数-迭代次数的关系图。从理论上来将,如果$\alpha$取得足够小,则每一次迭代代价函数都会减小。因此,如果代价函数呈现出其他的趋势,则往往说明是$\alpha$取得太大了。
|
||||
|
||||
需要指出的是,$\alpha$过大时,也可能会出现收敛速度慢的现象。
|
||||
|
||||
此外,显而易见的是,梯度下降法只能找到局部最优解,而非全局最优解。实际上,梯度下降法找到的解取决于初始位置的选择。然而,对于线性回归(linear regression)问题,则不存在这个问题,因为线性回归问题的代价函数是一个凸函数(convex function),即它只有一个极值点,该极值点就是它的全局最优解,因此使用梯度下降算法总是可以得到唯一的最优解。
|
||||
|
||||
> 梯度下降法的深入讨论。
|
||||
|
||||
对于多元的线性回归问题,如果不同的特征取值范围差别很大,比如$0 < x_1 < 1$,$0 < x_2 < 1000$,两者的取值范围查了1000倍。在使用梯度下降法时,参数$\theta_2$的变化量也将是$\theta_1$的1000倍,这将导致损失函数等高线图呈现扁平的椭圆状,梯度下降路径相对取值范围更大的特征对应的参数来回波动,导致收敛速度缓慢。但是关于这个的数学推导我仍然存在问题。
|
||||
|
||||
为了解决上面的问题,需要对取值相差比较大的特征进行特征缩放(feature scaling),使它们的取值都在1的附近。这样得到的等高线图就接近于圆形,收敛速度就要快多了。
|
||||
|
||||
此外还有一个方法是使这些特征的平均值都在0附近,相对于对特征进行了规范化。
|
||||
|
||||
> 在线性回归问题中,是通过平方损失函数(square error function)作为损失函数,来对参数进行优化的。老师说这样得到的是一条可能性最大的直线,是否可以用最大似然估计来解释这一点呢?
|
||||
|
||||
后来的总结中给出了数学推导。
|
||||
|
||||
> 多元线性回归(multivariate linear regression)的梯度下降法与规范方程法(normal equation)的一些问题。
|
||||
|
||||
不明白规范方程法正确性的证明,以及通过多元极值求得的结果与规范方程法是否一致?
|
||||
|
||||
用平方损失函数,利用多元极值求得的$\theta$与求解矩阵方程$Y = X\theta$所得到的结果一致,即$\theta = (X^X)^{-1}X^TY$,不得不让人思考两者是否有什么内在的联系?
|
||||
|
||||
规范方程法伪逆(pseudo-inversion)的数学推导;为什么求逆操作的时间复杂度是`O(n^3)`,其中`n`表示特征的数量。
|
||||
|
||||
为什么梯度下降法的时间复杂度是`O(kn^2)`,k是什么含义。
|
||||
|
||||
> 为什么是平方损失函数?
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,176 +0,0 @@
|
||||
神经网络算法总结
|
||||
==============
|
||||
|
||||
## 目录
|
||||
+ 摘要
|
||||
+ 前向传播算法
|
||||
- 模拟大脑
|
||||
- 模型表示
|
||||
- 为什么使用神经网络
|
||||
- 多分类问题
|
||||
+ 后向传播算法
|
||||
- 损失函数
|
||||
- 梯度的推导
|
||||
- 后向传播
|
||||
- 实际应用
|
||||
+ 梯度检查
|
||||
+ 向量化
|
||||
+ 随机初始化
|
||||
+ 总结
|
||||
|
||||
## 摘要
|
||||
|
||||
这篇文章介绍**神经网络算法**的基本原理以及实现方法。对原理的阐述仍然从三个方面来进行,即模型,策略与算法。这里的模型对应了**前向传播算法**(`forward propagation`,策略与算法则对应了**后向传播算法**(`backward propagation`),分别在文章的两段进行说明。最后,会将这两部分串联起来,给出实现神经网络的完整步骤。
|
||||
|
||||
## 前向传播算法
|
||||
|
||||
### 模拟大脑
|
||||
|
||||
让机器具有智能一直是计算机科学家的梦想,然而任何传统的算法都没能做到这一点。一种想法是,让机器模拟人类大脑的行为,从而使机器可以作出和人类相似的决策。
|
||||
|
||||
人类认知世界的过程可以作出以下抽象:我们的感觉器官,如眼睛、耳朵,获得了来自外界的光学信号与空气振动信号,通过大脑皮层相应位置的处理,转化为了我们可以辨识的物体以及语音的含义。大脑皮层的基本组成单位是神经元细胞,视觉皮层或者听觉皮层都是由许多个神经元细胞相互连接组成的。神经元细胞有两个重要的结构,即树突(`dentrite`)与轴突(`axon`),树突的功能是接受来自其他神经元传递来的信号,是输入单元;轴突则传递信号给其他的神经元,是输出单元。正是我们的感觉器官收到的信号引起的刺激,使得信号在不同神经元之间层层传递,才使得我们能够理解看到和听到的事物。
|
||||
|
||||
神经元之间传递信号的模式并不是先天决定或是一层不变的,比如刚出生的婴儿显然不能理解大人对它呢喃的话语,不同文化的人们看到同一事物也会产生不同的反映。因此,可以通过后天的训练与学习来改变神经元之间传递信号的模式,实际上,这也是人类学习和认识世界的机理所在。
|
||||
|
||||
同时,相关研究也指出,不同功能皮层的神经元之间也不存在差异。实验人员将视觉皮层损坏的小白鼠的视觉神经接到了听觉皮层,经过一段时间后,小白鼠就可以用听觉皮层来看到事物了,类似的实验在其他功能皮层也得到了成功。这说明,不同功能皮层的神经元本来是没有差异的,是后天的训练使得它们具有了自己特殊的信号传递方式。
|
||||
|
||||
可以看到,一个个简单的神经元,竟然使人类拥有了认识复杂世界的能力。造物主似乎偏爱这样的模式,即『复杂源于简单』,DNA 和蛋白质都是相关的例子。神经网络算法就是采用这样思路,即建立若干神经元,通过这些神经元的相互连接,来模拟人类大脑的行为。
|
||||
|
||||
### 模型表示
|
||||
|
||||
> 单个神经元模型
|
||||
|
||||
每一个神经元都是一个**逻辑回归单元**(`logistic regression unit`),它接受多个输入信号,产生一个输出信号,如下图所示:
|
||||
|
||||

|
||||
|
||||
不妨设
|
||||
|
||||
$$
|
||||
x = \left[
|
||||
\begin{matrix}
|
||||
x_0\\\\
|
||||
x_1\\\\
|
||||
x_2\\\\
|
||||
x_3
|
||||
\end{matrix}
|
||||
\right],\
|
||||
\theta = \left[
|
||||
\begin{matrix}
|
||||
\theta_0\\\\
|
||||
\theta_1\\\\
|
||||
\theta_2\\\\
|
||||
\theta_3
|
||||
\end{matrix}
|
||||
\right]
|
||||
$$
|
||||
|
||||
这里仍然有$x_0 = 1$,是手动添加的偏置单元。这样,$\hat{y}$满足逻辑回归的假设函数
|
||||
|
||||
$$
|
||||
\hat{y} = h_\theta(x) = sigmoid(\theta^T x)
|
||||
$$
|
||||
|
||||
> 神经网络模型
|
||||
|
||||
神经网络是由多个层次的神经元组成的,每个层次含有数量可以是不相同的神经元,一个简单的神经网络结构如下图所示:
|
||||
|
||||

|
||||
|
||||
可以看到,任何一个神经网络,都包含一个**输入层**,一个**输出层**,以及若干数量不确定的**隐含层**(`hidden layer`)。输入层一般用$x$或者$a^{(1)}$来表示,其中$x_j$或者$a_j^{(1)}$表示第$j$个特征的值;第一个隐含层用$a^{(2)}$来表示,$a_j^{(2)}$表示第一个隐含层的第$j$个神经元的值,后面的隐含层以此类推;用$L$表示神经网络含有的层次数量,这样输出层就可以用$y$或者$a^{(L)}$来表示了。
|
||||
|
||||
由于不同层次含有的神经元数量未必相同,通常使用$S_l$来表示第$l$层所包含的神经元数量,上图中,有$S_1 = 3, S_2 = 3, S_3 = 1$。
|
||||
|
||||
根据上面的神经元模型,相邻层次的神经元之间具有一定的信号传递关系,该传递关系实际上是对上一层次的全部神经元(加上偏置单元)的加权和做逻辑函数`sigmoid function`,各个神经元的权重可以理解为相邻两层神经元之间的练线上的数值,权重越大说明该神经元对于下一个层次的神经元的影响越大。相邻层次间神经元信号传递的权重可以构成一个矩阵,用$\Theta$表示,其中第$l$层和第$l + 1$层的权重矩阵可以表示为$\Theta^{(l)}$,它的每一个行向量都对应了第$l + 1$层的一个神经元的全部权重信息,因此该行向量的维度为$S_l + 1$,而$\Theta^{(l)}$一共具有$S_{l + 1}$行,一般地有
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
a_1^{(2)} = sigmoid(\Theta_{10}^{(1)}x_0 + \Theta_{11}^{(1)}x_1 +\Theta_{12}^{(1)}x_2 + \Theta_{13}^{(1)}x_3)\\\\
|
||||
a_2^{(2)} = sigmoid(\Theta_{20}^{(1)}x_0 + \Theta_{21}^{(1)}x_1 +\Theta_{22}^{(1)}x_2 + \Theta_{23}^{(1)}x_3)\\\\
|
||||
a_3^{(2)} = sigmoid(\Theta_{30}^{(1)}x_0 + \Theta_{31}^{(1)}x_1 +\Theta_{32}^{(1)}x_2 + \Theta_{33}^{(1)}x_3)
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
这就是相邻层次间神经元信号传递的模型。
|
||||
|
||||
> 前向传播算法
|
||||
|
||||
实际上,通过上面的公式已经可以窥见前向传播算法的大概了,将其写成矩阵形式,有
|
||||
|
||||
$$
|
||||
a^{(2)} = \left[
|
||||
\begin{matrix}
|
||||
a_1^{(2)}\\\\
|
||||
a_2^{(2)}\\\\
|
||||
a_3^{(2)}
|
||||
\end{matrix}
|
||||
\right] = sigmod(\Theta^{(1)} a^{(1)})
|
||||
$$
|
||||
|
||||
通过给$a^{(2)}$添加偏置项$a_0^{(2)} = 1$,可以写出输出$y$的表达式
|
||||
|
||||
$$
|
||||
y = a^{(3)} = sigmoid(\Theta_{10}^{(2)}a_0^{(2)} + \Theta_{11}^{(2)}a_1^{(2)} +\Theta_{12}^{(2)}a_2^{(2)} + \Theta_{13}^{(2)}a_3^{(2)})
|
||||
$$
|
||||
|
||||
同理,也可以把它写作矩阵形式
|
||||
|
||||
$$
|
||||
y = sigmoid(\Theta^{(2)}a^{(2)})
|
||||
$$
|
||||
|
||||
实际上,对于任意复杂结构神经网络,都可以定义以下前向传播算法
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
&a^{(1)} = x\\\\
|
||||
&z^{(2)} = \Theta^{(1)}a^{(1)}, a^{(2)} = sigmoid(z^{(2)})\\\\
|
||||
&z^{(3)} = \Theta^{(2)}a^{(2)}, a^{(3)} = sigmoid(z^{(3)})\\\\
|
||||
&\cdots\\\\
|
||||
&z^{(L)} = \Theta^{(L - 1)}a^{(L - 1)}, h_\Theta(x) = a^{(L)} = sigmoid(z^{L})
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
顾名思义,为了计算神经网络的输出,需要首先计算$a^{(2)}, a^{(3)}, \cdots$,最终才能计算出$h_\Theta(x)$,因此称为前向传播算法。训练神经网络的过程,其实就是训练这里的权重矩阵$\Theta$,这将在后面具体阐述。
|
||||
|
||||
> 为什么要使用神经网络?
|
||||
|
||||
神经网络是用于解决分类问题,而在此前的 [逻辑回归总结](../logistic_regression/logistic_regression.md) 中已经指出,借助**多项式回归**,逻辑回归模型理论上可以表示任意复杂的函数。既然如此,那为什么还要引入神经网络模型呢?
|
||||
|
||||
设想一种具体的应用场景,现在需要对一张`20x20`像素的图片进行分类,模型采用逻辑回归模型。图片分类问题显然不是一个线性划分问题,因此需要使用多项式回归,考虑使用所有的低于四次(含)的项,容易看出其实这并不是一个相当复杂的模型,但是仅考虑四次项就已经有$C_{400}^4$,即大约有10亿项。需要指出的是,这里使用的图片还只是`20x20`像素的,对于实际中常见分辨率的图片,如`480*360`,将具有更高维的特征向量,这是现有的计算能力不能满足的。
|
||||
|
||||
而使用神经网络模型则不然,因为不需要手工指定特征向量的形式,只将初始的400个像素点作为输入即可。这是因为,神经网络的多个隐藏层,足以形成足够复杂的模型,而无需手动引入多项式。
|
||||
|
||||
实际上,神经网络的一个主要优势在于,**神经网络可以自行学习特征**,而不需要像线性回归或者逻辑回归那样手工给定特征向量。对于上面图中的简单神经网络,如果只看隐藏层以及输出层,容易发现这就是一个逻辑回归模型,而此前的层次的功能,则可以理解为对特征的学习,隐藏层的值就是学到的特征向量。由于`sigmoid`函数是一个非线性函数,再加上多个隐藏层,神经网络模型足以学习到相当复杂的特征,而无需像逻辑回归那样手工指定高次多项式。
|
||||
|
||||
> 多分类问题。
|
||||
|
||||
如逻辑回归一样,神经网络也可以被推广到解决多分类问题,两者的方法如出一辙。对于逻辑回归而言,为了解决$n(n \ge 3)$分类问题,只需要训练$n$个二类分类问题的分类器即可;而对于神经网络模型,则只需要将输出层的神经元增加到$n$个,由于前面已经指出,神经网络的最后两个层次本质上就是逻辑回归模型,因此该方法本质上也是训练了$n$个二类分类器,与逻辑回归一致。多分类的神经网络结构如下所示:
|
||||
|
||||

|
||||
|
||||
但是两者在实现的细节上也有一些差异。对于$n$类分类问题,逻辑回归的输出的取值往往是$\{1, 2, \cdots, n\}$,而神经网络则要沿用向量表示法,例如当$n = 3$时,神经网络的输出应该表示为
|
||||
|
||||
$$
|
||||
y \in \{\left[
|
||||
\begin{matrix}
|
||||
1\\\\
|
||||
0\\\\
|
||||
0
|
||||
\end{matrix}
|
||||
\right],\
|
||||
\left[
|
||||
\begin{matrix}
|
||||
0\\\\
|
||||
1\\\\
|
||||
0
|
||||
\end{matrix}
|
||||
\right],\
|
||||
\left[
|
||||
\begin{matrix}
|
||||
0\\\\
|
||||
0\\\\
|
||||
1
|
||||
\end{matrix}
|
||||
\right]\}
|
||||
$$
|
||||
@@ -1,13 +0,0 @@
|
||||
Conclusions on String
|
||||
====================
|
||||
|
||||
+ `串`的特点:串长要远远大于字母表的大小
|
||||
|
||||
|
||||
## KMP
|
||||
> 为什么要取最长自匹配?
|
||||
快速右移 + 避免回退。(怎么理解)
|
||||
|
||||
## BM
|
||||
kmp是利用经验的高手,那么bm就是吸取教训的高手
|
||||
适用于大字符集
|
||||
@@ -1,5 +0,0 @@
|
||||
操作系统错题答案
|
||||
==============
|
||||
|
||||
1. C
|
||||
<br/>分页方式不会有外碎片
|
||||
@@ -1,7 +0,0 @@
|
||||
操作系统错题
|
||||
===========
|
||||
|
||||
1. 在使能分页机制的情况下,更合适的外碎片整理方法是()
|
||||
+ 紧凑(compaction)
|
||||
+ 分区对换(swapping in/out)
|
||||
+ 都不是
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |