Misaki`s blog

学习是一种态度


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

【Cpp】第十章-模板进阶

发表于 2019-08-12 | 分类于 Cpp
字数统计: 2.9k

模板进阶

  之前的博客已经介绍过模板的概念,这是Cpp在实现泛型编程中不可缺少的一环,在模板进阶的讨论中会着重于模板的更为高级的使用。

非类型模板参数

使用

  在模板中我们通常都是定义一个类型模板参数,在进行实例化的时候通过传入类型来实例化模板,但是模板中也可以定义非类型的模板参数。用一个封装的定长顺序表进行举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <assert.h>
using namespace std;
template<class T, size_t L>
class Array
{
public:
Array()
:_arr({0})
,_size(0)
{}
size_t Size() const
{
return _size;
}
T& operator[](size_t pos)
{
assert(pos < _size);
return _arr[pos];
}
void Push_Back(T data)
{
if(_size < L)
{
_arr[_size] = data;
_size++;
}
else
{
cout << "Array is full" << endl;
}
}
void Pop_Back()
{
if(!Empty())
{
_size--;
}
else
{
cout << "Array is empty" << endl;
}

}
bool Empty() const
{
return _size == 0;
}
private:
T _arr[L];
size_t _size;
};
int main()
{
Array<int, 20> arr;
arr.Push_Back(1);
arr.Push_Back(2);
arr.Push_Back(2);
arr.Push_Back(5);
arr.Push_Back(7);
arr.Push_Back(5);
arr.Pop_Back();
arr.Pop_Back();
arr.Pop_Back();
for(int i = 0; i < arr.Size(); i++)
{
cout << arr[i] << endl;
}
}

注意

  非类型模板参数在使用中有很严格的要求,它必须遵循以下规则,其实这块的要求在网上大部分博客中都说的十分模糊并且在我的实验下发现过于片面和局限,因此我给出我的总结和理解。
  1、非类型模板参数可以是整形,指针和引用。
  2、再给模板参数传参时要求其必须是一个常量表达式。
  3、如果模板参数是一个整形,那么在传参的时候可以传入字面值常量,也可以是全局/局部常量,可以是外部/内部链接(关于链接属性参考其他博客)。
  4、如果模板参数是一个指针或者引用,那么传参时要求,如果传入变量,变量必须是全局的,如果是常量常量必须是外部链接属性的,并且不能把动态对象的指针或引用传入。也就是说局部变量和内部链接属性的常量是不可以当作模板参数构造模板的,并且指针和引用还不能是动态对象的。
  以上是我个人在环境下多次实验得出的理解和总结,环境是gcc 6.3.0,如果有误区还请指出。

模板的特化

  模板可以封装不同的类型做相同的操作,然而有这么一种情况,我想要根据不同的类型做出不同于原来模板的操作,这就需要用到模板的特化这个语法。特化就是在原模版的基础上,根据特殊类型进行特殊化的实现方式。

函数模板的特化

  函数模板特化要求当然要有基本的函数模板,在特化时格式要求template后跟一对尖括号,并且函数名后尖括号内写特化的模板参数。参数及函数体中的模板参数必须全部替换为特化的模板实参,不然会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
//为了验证函数模板与类模板
//先写一个函数模板
template<class T>
T Add(T a, T b)
{
return a + b;
}
//特化处理
template<>
int Add<int>(int a, int b)
{
cout << "specialization" << endl;
return a + b;
}
int main()
{
cout << Add(1, 2) << endl;
}


specialization
3

  这样的特化写法是最为规范的写法,就像是函数模板的显示实例化一样,当然我们也可以将<int>不要,只要能让模板知道你再特化哪一种情况即可,但是要与模板完全符合,不然是编不过的。
  还有一种更为简单的方式,及利用我在模板初阶中提到的模板匹配规则。模板匹配是在所有同名函数都不符合调用的情况下才会进行实例化,因此我们可以用类似重载的情况写同名函数来进行处理,在调用时会优先调用非模板函数。这种情况并不能算是模板的特化处理,但是可以用来处理一些模板无法匹配的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;
template<class T>
T Add(T& a, T& b)
{
return a + b;
}
template<>
int Add(int& a, int& b)
{
cout << "specialization" << endl;
return a + b;
}
int Add(int a, int b)
{
cout << "overload" << endl;
return a + b;
}
int main()
{
int a = 1;
int b = 2;
cout << Add(a, b) << endl;//调用重载的Add
cout << Add<int>(a, b) << endl;//调用特化的Add
}


overload
3
specialization
3

  在这个例子中可以看出根据模板匹配规则确实优先调用了我们重载的函数,我们只有显示实例化才会调用模板。

类模板的特化

  类模板特化与函数模板类似,但是由于类模板是无法通过其他方式识别模板参数的类型的,因此我们必须通过<>来显示实例化才能进行特化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
using namespace std;
//类模板
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _data1;
T2 _data2;
};
//特化处理
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _data1;
char _data2;
};
int main()
{
Data<int, int> data1;
Data<int, char> data2;
}


Data<T1, T2>
Data<int, char>

特化的种类

全特化

  全特化就是将模板参数全部进行实例特化的情况。这里用类模板举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _data1;
T2 _data2;
};
//这样的把所有的模板参数都进行实例特化的就叫全特化
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _data1;
char _data2;
};
int main()
{
Data<int, int> data1;
Data<int, char> data2;
}


Data<T1, T2>
Data<int, char>

偏特化

  偏特化就是全特化以外的情况,并没有将所有的模板参数全部都实例化为某一特殊情况。偏特化又有两种情况。
  1、部分特化:部分特化是将模板参数中一部分模板参数进行实例化特化的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
using namespace std;
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _data1;
T2 _data2;
};
//这样的把所有的模板参数都进行实例特化的就叫全特化
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _data1;
char _data2;
};
//这里就是一个部分特化,只将第一个参数进行实例化的情况
template<class T>
class Data<int, T>
{
public:
Data()
{
cout << "Data<int, T>" << endl;
}
};
int main()
{
Data<int, int> data1;//这里会调用部分特化
Data<int, char> data2;//这里调用全特化
}



Data<int, T>
Data<int, char>

  通过这个例子还可以证实如果实例化同时符合全特化和部分特化的特化情况,则会优先调用全特化,之后才会考虑部分特化的情况。
  2、将参数进一步限制的特化:
  这个类型的特化是将参数进行了进一步的限制,我们可以将参数限制为指针类型或者引用,在这种情况下才会调用这种类型的特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
using namespace std;
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _data1;
T2 _data2;
};
//这样的把所有的模板参数都进行实例特化的就叫全特化
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _data1;
char _data2;
};
//这里就是一个部分特化,只将第一个参数进行实例化的情况
template<class T>
class Data<int, T>
{
public:
Data()
{
cout << "Data<int, T>" << endl;
}
};
//将两个参数类型限制为指针类型,如果两个参数都是指针类型则调用这个特化
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
};
int main()
{
Data<int, int> data1;//这里会调用部分特化
Data<int, char> data2;//这里调用全特化
Data<int*, char*> data3;//这里调用类型限制的特化
}

类型萃取

  类型萃取是通过特化的方式使模板可以识别不同的类型从而使用不同的处理方法。比如说我们要写一个拷贝函数,对于内置类型我们直接调用memcpy()函数拷贝内存,而对于自定义类型我们可能需要使用自定义的其他拷贝方法进行深拷贝,这时候就可以使用类型萃取的方式来区别内置类型和自定义类型达到不同处理方式的结果。
  这并不是一种新的语法,更像是一种设计模式,在STL中就有所体现。

模板的分离编译

  什么是分离编译模式?分离编译模式就是我们通常在写项目是方便项目管理时所使用的将函数和类的声明放进.h文件中而在.cpp文件中写类定义和类成员函数定义的模式。这种方法可行就是应为我们将声明写入.h而在需要使用的地方引入头文件,在链接过程中编译器会根据声明找到具体实现定义的地址完成链接。

模板不支持分离编译

  但是这里要说的是,无论是函数模板还是类模板在没有实例化之前都是没有具体代码的,那么也就没有具体定义的地址,我们根据声明也就无法链接到定义的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}

  以上这个例子中的代码时编不过的,原因就是模板不支持分离编译,无法连接。

如何解决?

  解决方法也很简单,最简单的就是将定义和声明都写到头文件中可以了,这样就省去了链接这个步骤,也就不存在错误了。

【Cpp】第九章-STL_stack类和queue类

发表于 2019-08-05 | 分类于 Cpp
字数统计: 3.8k

stack类和queue类

  stack和queue以及priority_queue(优先级队列)是STL中三大容器适配器,将其称为容器适配器是因为其在底层只是对现有容器进行的了封装而并没有重新实现。因此在容器适配器中都有让传入容器的模板参数。

容器适配器

  适配器是一种设计模式,在GOF的《设计模式:可复用面向对象软件的基础》中是这样说的:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。而容器适配器就是将常见容器的接口进行封装,使之成为我们需要的结构。因此在容器适配的模板参数中我们可以传入我们想要使用的容器作为模板参数进行封装,但是作为适配器的实现容器它们也需要满足一定的要求才可以作为模板参数。

1
2
3
4
5
//Container就是底层实现的容器的模板参数
template <class T, class Container = deque<T> > class queue;
template <class T, class Container = deque<T> > class stack;
template <class T, class Container = vector<T>,
class Compare = less<typename Container::value_type> > class priority_queue;

stack类

  stack和我们数据结构中的栈实现了同样的功能,后进先出是其最大的特点。它只能从容器的一端进行元素的插入和提取,来满足栈的相关功能。
  它底层的容器可以是标准中的容器类也可以是其他的容器类,但是无论如何这些容器类必须满足以下要求。
  1、empty(),判空操作。
  2、back(),获取尾部元素。
  3、push_back(),尾插。
  4、pop_back(),尾删。
  满足这些要求的容器类才可以被传入,如果没有传入容器的话则默认使用deque。

常用接口

1
2
3
4
5
6
7
8
9
10
stack(const container_type &ctnr = container_type()); //构造空的栈
bool empty() const; //检测stack是否为空
size_type size(); //const 返回stack中元素的个数
value_type &top(); //返回栈顶元素的引用
const value_type &top() const; //返回栈顶元素的const引用
void push(const value_type &val); //将元素val压入stack中
void pop(); //将stack中尾部的元素弹出
template <class... Args>
void emplace(Args &&... args); //(C++11) 在stack的栈顶构造元素
void swap(stack &x); //(C++11) 交换两个栈中的元素

栈的应用

最小栈

  力扣:
https://leetcode-cn.com/problems/min-stack/submissions/
  这道题就是用栈区实现一个可以返回当前栈中最小值的栈,方法有很多种,这里只提供一种方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class MinStack {
public:
/** initialize your data structure here. */
MinStack()
:_min(INT_MAX)
{}

void push(int x) {
if(x < _min)
{
_min = x;
}
_elem.push_back(x);
}

void pop() {
if(top() == _min)
{
_min = INT_MAX;
for(int i = 0; i < _elem.size() - 1; i++)
{
if(_elem[i] < _min)
{
_min = _elem[i];
}
}
}
_elem.pop_back();
}

int top() {
return _elem[_elem.size() - 1];
}

int getMin() {
return _min;
}
private:
std::vector<int> _elem;
int _min;
};

/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(x);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/

栈的压入、弹出序列

  牛客网:
https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&&tqId=11174&rp=1&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking
  这道题用到了栈的弹出序列是否匹配压入序列的算法,大致思路是用一个栈进行模拟,并且设立用于分别遍历弹出和压入序列的下标,只要栈顶元素不等于当前遍历到的弹出序列的元素,就将压入序列当前元素压入栈,并且遍历压入序列的下一个元素,如果相等,则将栈顶元素弹出,并且遍历弹出序列的下一个元素。如果当弹出序列还没有遍历完毕而压入序列也始终找不到下一个可以和弹出序列进行匹配的元素则不匹配,返回false,如果弹出序列遍历完毕,则表示匹配,返回true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV) {
int pushIndex = 0;
int popIndex = 0;
//用栈进行模拟
stack<int> s;
//遍历弹出序列
while(popIndex < popV.size())
{
//栈为空或者栈顶元素不等于当天弹出序列遍历到的元素
while(s.empty() || s.top() != popV[popIndex])
{
//压入序列还有元素则压入
if(pushIndex < pushV.size())
{
s.push(pushV[pushIndex]);
pushIndex++;
}
//压入序列全部压入无法完成匹配
else
{
return false;
}
}
//栈顶元素和弹出序列当前元素相同则往后继续遍历
s.pop();
popIndex++;
}
return true;
}
};

逆波兰表达式求值

  力扣:
https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/submissions/
  逆波兰表达式也成为后缀表达式,这里的逆波兰表达式求值由于不涉及括号所以比较简单,我们只需要将遇到的操作数放入栈,遇到操作符则取出栈顶两个元素进行计算,然后将结果再次放入栈最后取出栈中最后一个元素就是答案。不过这里要注意负数的处理,以及从栈中取出的两个操作数哪个是前操作数哪个是后操作数的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> s;
for(int i = 0; i < tokens.size(); i++)
{
if(tokens[i][0] <= '9' && tokens[i][0] >= '0')
{
s.push(atoi(tokens[i].c_str()));
}
//要处理负数
else if(tokens[i][0] == '-' && tokens[i][1] != '\0')
{
string str = tokens[i].substr(1);
int temp = -1 * atoi(str.c_str());
s.push(temp);
}
else
{
int num1 = s.top();
s.pop();
int num2 = s.top();
s.pop();
switch(tokens[i][0])
{
case '+':
s.push(num2 + num1);
break;
case '-':
s.push(num2 - num1);
break;
case '*':
s.push(num2 * num1);
break;
case '/':
s.push(num2 / num1);
break;
}
}
}
return s.top();
}
};

queue类

  和stack相反,queue有着先进先出的特性,这使得他能够按照顺序完成某一功能。它从容器的一端插入另一端删除,因此它的模板参数中的容器要求有以下功能:
  1、empty(),判空
  2、size(),长度
  3、back(),获取尾部元素
  4、front(),获取头部元素
  5、push_back(),尾插
  6、pop_front(),头删
  如果没有传入容器的话则默认使用deque。

常用接口

1
2
3
4
5
6
7
8
9
10
11
12
queue(const container_type &ctnr = container_type()); //构造空的队列
bool empty() const; //检测队列是否为空,是返回true,否则返回false
size_type size() const; //返回队列中有效元素的个数
value_type &front(); //返回队头元素的引用
const value_type &front() const; //返回队头元素的const引用
value_type &back(); //返回队尾元素的引用
const value_type &back() const; //返回队尾元素的cosnt引用
void push(value_type & val); //在队尾将元素val入队列
void pop(); //将队头元素出队列
template <class... Args>
void emplace(Args && ... args)(C++ 11); //在队尾构造元素
void swap(queue & x); //交换两个队列中的元素

队列的应用

用队列实现栈

  力扣:
https://leetcode-cn.com/problems/implement-stack-using-queues/
  利用栈进行层序遍历类似于bfs,即广度优先搜索,我们在进行遍历时需要将每一个结点的儿子都压入栈,只要栈不为空就一直遍历下去。但是这个题要求要区分每一层,则我们需要在遍历每一层前都拿到当前队列中结点的个数,则是这一层要遍历的结点数,我们这一层就只遍历这些结点即可。我们甚至由此可以得出N叉树的层序遍历,都可以用类似的思路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
queue<TreeNode*> q;
//根结点为空直接返回
if(root == nullptr)
{
return res;
}
q.push(root);
while(!q.empty())
{
vector<int> line;
int size = q.size();
for(int i = 0; i < size; i++)
{
TreeNode* node = q.front();
if(node->left != nullptr)
{
q.push(node->left);
}
if(node->right != nullptr)
{
q.push(node->right);
}
q.pop();
line.push_back(node->val);
}
res.push_back(line);
}
return res;
}
};

priority_queue类

  priority_queue称为优先级队列,它的出队元素永远是队列中优先级最高的那一个,它的底层是用一个堆来完成的,堆顶就是优先级最高的元素。
  优先级队列在每次插入删除元素后都会分别调用算法push_heap()和pop_heap()重新建堆,并且还会和容器中的push_back()和pop_back()结合。同时优先级队列中有一个Compare的模板参数,用于传入一个仿函数,建堆会用这个仿函数按照严格弱序进行建堆。
  所谓仿函数则是一个类中重载了operator()使其实例化的对象可以像函数一样进行调用,它的特点是可以使类作为模板参数一部分传入模板内,并且使其实例化出的对象可以当作函数使用。标准中给了两个类提供了仿函数的功能,less和greater。
  如果在优先级队列中存放自定义类型的数据,要求自定义类型必须重载operator<或operator>取决于Compare的类型,这两个重载在仿函数中将会进行调用用于判断和比较。
  同样的优先级队列中的容器也必须满足一些要求才能完成功能:
  1、empty(),判空
  2、size(),长度
  3、front(),获取头部元素
  4、push_back(),尾插
  5、pop_back(),尾删
  容器还需要支持随机访问迭代器,以便始终在内部保持堆结构。
  为什么是这些接口?front()用于在建堆和堆调整后可以获得堆顶元素,即优先级最高的元素,即top();push()在给堆插入元素时是从容器的尾部插入,因此需要push_back()进行尾插,尾插后再用push_heap()调整堆;pop()在出队时要先调用pop_heap(),这个接口会将堆顶元素与堆的最后一个元素交换位置,并且对堆的除最后一个元素进行调整,这个过程类似于堆排序。之后再调用容器的pop_back()即可将原本堆顶优先级最高的元素出队。

常用接口

1
2
3
4
5
6
7
8
9
10
priority_queue(const Compare &x = Compare(),
const Container &y = Container()); //构造一个空的优先级队列
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last,
const Compare &comp = Compare(),
const Container &ctnr = Container()); //用[first, last)区间中的元素构造优先级队列
bool empty() const; //检测优先级队列是否为空,是返回true,否则返回false
const value_type &top() const; //返回优先级队列中最大(最小元素),即堆顶元素
void push(const T &x); //在优先级队列中插入元素x
void pop(); //删除优先级队列中最大(最小)元素,即堆顶元素

  默认情况下,类模板中Compare的类型为less,实例化出来的对象重载时判断的为是否小于,因此会创建大堆,队列出队返回的值也是队中优先级最高的。

优先级队列的应用

  优先级队列底层是堆的实现,因此优先级队列擅长于结局堆能够解决的问题,例如TopK问题。

数组中的第K个最大的元素

  力扣:
https://leetcode-cn.com/problems/kth-largest-element-in-an-array/
  这道题就利用优先级队列找到第k个大的元素即可,也可以先排序后再找。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int> que(nums.begin(), nums.end());
for(int i = 0; i < k - 1; i++)
{
que.pop();
}
return que.top();
}
};

前K个高频元素

力扣:
https://leetcode-cn.com/problems/top-k-frequent-elements/
  这道题做起来不难,但是是对优先级队列较为综合的运用,我们要考虑到TOPK问题中找前K大要用小根堆,还要考虑到要将出现次数与元素本身联系起来要在优先级队列中存储pair。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
map<int, int> m;
for(int i = 0; i < nums.size(); i++)
{
m[nums[i]]++;
}
//topK问题求前K大一定要用小根堆,维护K个数据
//用大根堆要放入全部数据费时费空间
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> que;
for(auto e : m)
{
if(que.size() >= k)
{
if(e.second > que.top().first)
{
que.pop();
que.push(make_pair(e.second, e.first));
}
}
else
{
que.push(make_pair(e.second, e.first));
}
}
vector<int> res(k);
while(!que.empty())
{
res[que.size() - 1] = que.top().second;
que.pop();
}
return res;
}
};

实现

  依旧是实现常用接口,了解底层原理。

stack类的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#pragma once
#include <iostream>
#include <deque>
template<class T, class Container = std::deque<T>>
class Stack
{
public:
void Push(const T& data)
{
_con.push_back(data);
}
void Pop()
{
_con.pop_back();
}
bool Empty()
{
return _con.empty();
}
size_t Size()
{
return _con.size();
}
T& Top()
{
return _con.back();
}
const T& Top() const
{
return _con.back();
}
private:
Container _con;
};

queue的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#pragma once
#include <iostream>
#include <deque>
template<class T, class Container = std::deque<T>>
class Queue
{
public:
void Push(const T& data)
{
_con.push_back(data);
}
void Pop()
{
_con.pop_front();
}
size_t Size()
{
return _con.size();
}
bool Empty()
{
return _con.empty();
}
T& Back()
{
return _con.back();
}
const T& Back() const
{
return _con.back();
}
T& Front()
{
return _con.front();
}
const T& Front() const
{
return _con.front();
}
private:
Container _con;
};

priority_queue的实现

  在SGI版本的priority_queue中建堆以及调整堆都是调用的algorithm中的算法函数完成的,但是在模拟实现中我们完全自己实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#pragma once
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
template<class T>
struct Less
{
bool operator()(const T& obj1, const T& obj2)
{
return obj1 < obj2;
}
};
template<class T>
struct Greater
{
bool operator()(const T& obj1, const T& obj2)
{
return obj1 > obj2;
}
};

template<class T, class Container = std::vector<T>, class Compare = Less<T>>
class Priority_queue
{
public:
//向下调整
void AdjustDown(size_t parent)
{
size_t child = parent * 2 + 1;
Compare com;
while(child < _con.size())
{
if(child + 1 < _con.size() && com(_con[child], _con[child + 1]))
{
child++;
}
if(com(_con[parent], _con[child]))
{
std::swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//向上调整
void AdjustUp(size_t child)
{
size_t parent = (child - 1) / 2;
Compare com;
while(child > 0)
{
if(com(_con[parent], _con[child]))
{
std::swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void Push(const T& data)
{
_con.push_back(data);
//模拟push_heap()的功能
AdjustUp(_con.size() - 1);
}
void Pop()
{
//模拟pop_heap()的功能
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
size_t Size()
{
return _con.size();
}
bool Empty()
{
return _con.empty();
}
T& Top()
{
return _con.front();
}
const T& Top() const
{
return _con.front();
}
//private:
Container _con;
};

【Cpp】第八章-STL_deque类

发表于 2019-07-30 | 分类于 Cpp
字数统计: 1.4k

deque类

基础介绍

  deque是双端队列,它提供了和vector类似的接口但是底层的实现与vector完全不同,vector底层用三个指针指向数组的起点,尾部和总容量的尾部,并且所有元素都是连续的,但是在deque中所有元素并不一定都是在连续的内存空间上的。deque在底层实现上是将一个连续的空间分段进行管理,并将它们的首地址用一个指针数组进行管理,这样特殊的存储结构使得它在头部和尾部增加元素比vector更加高效,但是底层实现更为复杂,存储了很多额外信息。如果抛去在头部和尾部增加元素,在中间任意位置添加元素,它的效率比vector更高,但是比list要低。

常用接口

构造函数

1
2
3
4
deque();                                                  //构造空的双端队列
deque(size_type n, const value_type &val = value_type()); //用n个值为val的元素构造双端队列
deque(InputIterator first, InputIterator last); //用[first, last)的区间构造双端队列
deque(const deque &x); //双端队列的拷贝构造函数

迭代器相关

  由于deque在内存上并不完全是连续的因此想要保持deque的连续性,这个任务就落到了迭代器身上。在底层实现上,deque将一段一段连续的内存称为一个缓冲区(buffer),并将这些缓冲区的首尾地址存储在一个map中用以映射,map中一个存储缓冲区的地址对应一个结点(node)信息用于标记这个键值对,这样就构建好了基础架构。在迭代器中存储了4个信息,分别是当前结点(cur),当前缓冲区的头(first),当前缓冲区的尾(last)以及在map中用以标记当前缓冲区的地址的结点(node)信息。并且在deque内部已经存储好了两个迭代器start和finish用于标记deque的头和尾元素。这样即可完成将一段一段连续的空间在逻辑结构上构成一段连续空间的目的。
  当从头遍历deque时,start迭代器中first和last已经从map中找到了第一个结点的缓冲区首尾信息并进行了保存,于是cur就从first开始遍历这个缓冲区,当遍历到last时就重新到map中寻找写一个结点的缓冲区收尾地址并且替换掉原来first和last值,继续遍历,这样即可完成遍历直到最后一个结点也遍历完。

1
2
3
4
5
6
7
8
iterator begin();                       //返回deque起始位置迭代器
iterator end(); //返回deque最后一个元素下一个位置的迭代器
reverse_iterator rbegin(); //返回deque起始位置的反向迭代器(即end())
reverse_iterator rend(); //返回deque最后一个元素下一个位置的反向迭代器(begin())
const_iterator cbegin() const; //返回deque起始位置的const迭代器
const_iterator cend() const; //返回deque最后一个元素下一个位置的const迭代器
const_reverse_iterator crbegin() const; //返回deque起始位置的const反向迭代器(即crend())
const_reverse_iterator crend() const; //返回deque最后一个元素下一个位置的const反向迭代器(crbegin())

容量相关

1
2
3
size_type size() const;               //返回deque中有效元素个数
bool empty() const; //检测deque是否为空,是返回true,否则返回false
void resize(size_type sz, T c = T()); //将deque中的元素改变到sz,多出的空间用c填充

增删改查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
reference operator[](size_type n);                         //返回deque中n位置上元素的引用
const_reference operator[](size_type n) const; //返回deque中n位置上元素的const 引用
reference front(); //返回deque中首元素的引用
const_reference front() const; //返回deque中首元素的const引用
reference back(); //返回deque中最后一个元素的引用
const_reference back() const; //返回deque中最后一个元素的const引用
void push_back(const value_type &val); //deque尾部插入元素val
void pop_back(); //删除deque尾部元素
void push_front(const value_type &val); //deque头部插入元素val
void pop_front(); //删除deque头部元素
iterator insert(iterator position, const value_type &val); //在deque的position位置插入值为val的元素
void insert(iterator position, size_type n,
const value_type &val); //在deque的position位置插入n个值为val的元素
void insert(iterator position, InputIterator first, InputIterator last); //在deque的position位置插入[first, last)区间中的元素
iterator erase(iterator position); //删除deque中position位置的元素,并返回该位置的下一个位置
iterator erase(iterator first, iterator last); //删除deque中[first, last)区间中的元素,并返回last位置
void swap(deque & x); //交换两个deque中的内容
void clear(); //将deque中的元素清空
iterator emplace(const_iterator position, Args && ... args); //在deque的position位置构造元素,将元素所需内容通过参数类表传入
void emplace_front(Args && ... args); //在deque的头部构造元素,元素的参数通过参数列表传入
void emplace_back(Args && ... args); //在deque的尾部构造元素,元素的参数通过参数列表传入

综合运用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <deque>
using namespace std;
int main()
{
deque<int> deq;
deq.push_front(1);
deq.push_back(2);
deque<int>::iterator it = deq.begin();
it = deq.insert(it, 0);
while(it != deq.end())
{
cout << *it << endl;
it++;
}
it = deq.erase(--it);
it = deq.begin();
while(it != deq.end())
{
cout << *it << endl;
it++;
}
}

总结

  双端队列deque是一个设计并不算成功的容器,如果要随机访问单纯的查询多一点可以用vector而且更加方便,如果需要频繁插入那么list效率又会跟高,因此deque并不常用,其最常用的地方就是在作为适配器stack和queue的底层存储容器。

【Cpp】第七章-STL_list类

发表于 2019-07-29 | 分类于 Cpp
字数统计: 2.2k

list类

基础介绍

  list类是STL中封装的链表模板类,并且底层实现是以双向链表作为基础进行封装的。在数据结构中,线性存储结构中主要分为顺序表和链表,前者在物理结构上拥有连续的内存空间和地址,在STL中vector和string都是使用了这种结构,其最大的特点就是方便进行随机访问并且尾插和尾删都能达到O1的时间复杂度并且使用方便,而链表作为物理结构上内存空间不连续的数据结构,其最大的特点就是方便在任何位置就行插入删除,list就是建立在此数据结构上封装出的模板类。

常用接口

构造函数

1
2
3
4
list();                                                  //构造空的list
list(size_type n, const value_type &val = value_type()); //构造的list中包含n个值为val的元素
list(const list &x); //拷贝构造函数
list(InputIterator first, InputIterator last); //用[first, last)区间中的元素构造list

迭代器相关

1
2
3
4
5
6
7
8
begin();   //返回第一个元素的迭代器
end(); //返回最后一个元素下一个位置的迭代器
rbegin(); //返回第一个元素的reverse_iterator,即end位置
rend(); //返回最后一个元素下一个位置的reverse_iterator,即begin位置
cbegin(); //(C++11) 返回第一个元素的cosnt_iterator
cend(); //(C++11) 返回最后一个元素下一个位置的const_iterator
crbegin(); //(C++11) 即crend()位置
crend(); //(C++11) 即crbegin()位置

容量相关

1
2
bool empty() const;  //检测list是否为空,是返回true,否则返回false
size_t size() const; //返回list中有效节点的个数

增删查改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
reference front();                      //返回list的第一个节点中值的引用
const_reference front() const; //返回list的第一个节点中值的const引用
reference back(); //返回list的最后一个节点中值的引用
const_reference back() const; //返回list的最后一个节点中值的const引用
void push_front(const value_type &val); //在list首元素前插入值为val的元素
void pop_front(); //删除list中第一个元素
void push_back; //(const value_type& val)在list尾部插入值为val的元素
void pop_back(); //删除list中最后一个元素
template <class... Args>
void emplace_front(Args && ... args); //(C++11)在list第一个元素前根据参数直接构造元素
template <class... Args>
void emplace_back(Args && ... args); //(C++11)在list最后一个元素后根据参数直接构造元素
template <class... Args>
iterator emplace(const_iterator position, Args && ... args); //(C++11)在链表的任意位置根据参数直接构造元素
iterator insert(iterator position, const value_type &val); //在list position 位置中插入值为val的元素
void insert(iterator position, size_type n, const value_type &val); //在list position位置插入n个值为val的元素
void insert(iterator position, InputIterator first, InputIterator last); //在list position位置插入[first, last)区间中元素
iterator erase(iterator position); //删除list position位置的元素
iterator erase(iterator first, iterator last); //删除list中[first, last)区间中的元素
void swap(list & x); //交换两个list中的元素
void resize(size_type n, value_type val = value_type()); //将list中有效元素个数改变到n个,多出的元素用val填充
void clear(); //清空list中的有效元素

emplace与insert

  在STL很多接口中我们都发现有一个emplace()的接口也是用来进行插入的,那么它与insert()和push_back()有什么区别呢?以下用vector举个例子,从接口调用上就能看出区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <vector>
using namespace std;
int main()
{
struct Foo
{
Foo(int n, double x)
:_n(n)
,_x(x)
{

}
int _n;
double _x;
};
vector<Foo> v;
v.emplace(v.begin(), 42, 3.1416); // 没有临时变量产生
v.insert(v.begin(), Foo(42, 3.1416)); // 需要产生一个临时变量
v.insert(v.begin(), {42, 3.1416}); // 需要产生一个临时变量
for(int i = 0; i < v.size(); i++)
{
cout << v[i]._n << " " << v[i]._x << endl;
}
}


42 3.1416
42 3.1416
42 3.1416

  在进行内置类型和拥有单参构造函数的类型的插入时我们只传入一个参数往往很难发现区别,但在我们必须传入多个参数才能进行插入时我们就会发现在使用insert()这些接口时我们必须插入与容器中元素类型相同的元素才能完成接口调用,这让我们不得不构造一个临时匿名对象,而emplace则不需要,我们只要按照构造一个对象那样给构造元素的参数即可省去了构造匿名对象的过程。而这其中需要用到C++11中的新标准变参模板和完美转发,因此emplace使用的时候要求编译器支持C++11标准。
  emplace不光是调用接口上有所不同,在有的时候也可以提高我们的效率。例如在list中如果利用insert传入一个元素的时候我们就需要先调用元素的构造函数构造元素,然后再调用Node的构造函数以及该元素的拷贝构造函数构造一个拥有该元素值的结点,才能进行插入,而emplace则可以在创建结点时直接利用原本要构造临时元素对象的参数来直接构造结点,并且直接构造结点中的元素对象,相当于少了一次元素的拷贝构造,并且省去了构造临时对象,效率更高。

list迭代器失效

  对于list这种链式结构,在插入后是不会迭代器失效的,因为原先位置的迭代器依旧指向原来的结点,不会因为添加结点导致指向位置的变动。而在删除后迭代器原本指向的结点内存被释放,因此删除后删除结点的迭代器失效,但是其他结点迭代器指向不变因此不会发生迭代器失效。
  总结:list只有在删除节点后才会发生迭代器失效,并且只有删除结点的迭代器失效。

实现

  list采用链式结构存储,因此实现起来要稍微麻烦一些,迭代器也需要额外进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#include <assert.h>
//定义结点类
template<class T>
struct ListNode
{
ListNode<T>* _prev;
ListNode<T>* _next;
T _data;
ListNode(const T& data = T())
:_prev(nullptr)
,_next(nullptr)
,_data(data)
{}
};
//由于const_iterator与iterator除了在迭代器返回值上不一样外其他要求完全一样
//因此这里要进行实现时可以考虑定义两个类
//但是这里使用一种取巧的方法我们将返回值类型当作模板参数传入模板中
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
//构造函数
ListIterator(Node* node)
:_node(node)
{}
//operator* 为了让其可以和指针一样使用,返回引用
Ref operator*()
{
return _node->_data;
}
//operator-> 为了让其可以和指针一样使用,返回指针
//这里实际调用it->只能取到数据的指针,所以正常来说得写成it->->
//但是经过编译器优化,只用写it->就可以取到值了
Ptr operator->()
{
return &(_node->_data);
}
Self operator++()
{
_node = _node->_next;
return _node;
}
Self operator++(int)
{
Self temp(_node);
_node = _node->_next;
return temp;
}
Self operator--()
{
_node = _node->_prev;
return _node;
}
Self operator--(int)
{
Self temp(_node);
_node = _node->_prev;
return temp;
}
bool operator!=(Self it)
{
return _node != it._node;
}
Node* _node;
};
//构建一个带头结点双向循环链表来模拟实现list
template<class T>
class List
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
//构造函数j
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
//拷贝构造
List(const List& list)
:_head(new Node)
{
_head->_next = _head;
_head->_prev = _head;
const_iterator it = list.begin();
while(it != list.end())
{
Push_back(*it);
it++;
}
}
//operator=重载
List& operator=(const List& list)
{
List listTemp = list;
Swap(listTemp);
}
//交换
void Swap(List& list)
{
std::swap(_head, list._head);
}
//析构函数
~List()
{

while(_head->_next != _head)
{
Pop_back();
}
}
iterator begin()
{
return _head->_next;
}
const_iterator begin() const
{
return _head->_next;
}
iterator end()
{
return _head;
}
const_iterator end() const
{
return _head;
}
iterator Erase(iterator& it)
{
assert(it._node != nullptr);
Node* pTemp = it._node;
Node* pNew = pTemp->_next;
//it->_node = pTemp->_next;
pTemp->_prev->_next = pTemp->_next;
pTemp->_next->_prev = pTemp->_prev;
delete pTemp;
return pNew;
}
//插入
iterator Insert(iterator& it, const T& data = T())
{
assert(it._node != nullptr);
Node* newNode = new Node(data);
newNode->_next = it._node;
newNode->_prev = it._node->_prev;
it._node->_prev->_next = newNode;
it._node->_prev = newNode;
return newNode;
}
//尾插
void Push_back(const T& data)
{
assert(_head != nullptr);
Node* tail = _head->_prev;
Node* newNode = new Node(data);
newNode->_next = _head;
newNode->_prev = tail;
tail->_next = newNode;
_head->_prev = newNode;
}
//尾删
void Pop_back()
{
assert(_head != nullptr);
if(_head->_next == _head)
{
return;
}
Node* tail = _head->_prev;
tail->_prev->_next = _head;
_head->_prev = tail->_prev;
delete tail;
}
private:
Node* _head;
};

【算法】第二章-搜索

发表于 2019-07-27 | 分类于 算法
字数统计: 2.2k

搜索

深度优先搜索(DFS)

  深度优先搜索是使用递归的方式以深度为主逐个探索遍历每种情况,在排列组合,迷宫问题中十分常用。深度优先搜索思想简单,但是由于使用递归,要求我们遍历时探索的必须深度有限。不然有可能会使栈溢出。还要注意有时我们在使用深度优先搜索时情况过多,而大部分是无用解时就需要套入剪枝。
  模型:

1
2
3
4
5
6
DFS()
{
//1.判断边界,如果已经到达搜索的最深,则回退尝试其他可能
//2.尝试当下的每一种可能
//3.确定一种可能后,继续下一步
}

例1 员工的重要度

  力扣:
https://leetcode-cn.com/problems/employee-importance/submissions/

解析

  抽象模型。

1
2
3
4
5
6
DFS(id)
{
//1.获取当前员工的重要度
//2.累加每一个下属(for)的重要度,DFS(下属id)
//3.return累加的重要度
}

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int DFS(unordered_map<int, Employee*> em, int id)
{
int curRet = em[id]->importance;
for(auto& e : em[id]->subordinates)
{
curRet += DFS(em, e);
}
return curRet;
}
int getImportance(vector<Employee*> employees, int id) {
if(employees.empty())
{
return 0;
}
unordered_map<int, Employee*> em;
for(auto& e: employees)
{
em[e->id] = e;
}
return DFS(em, id);
}
};

例2 图像渲染

  力扣:
https://leetcode-cn.com/problems/flood-fill/

解析

  抽象模型。

1
2
3
4
5
6
7
8
9
10
11
DFS()
{
//1.nx,ny染色
//2.处理上,下,左,右4个点
{
//以深度优先逐个方向遍历周边4个点
//新位置颜色符合,且没有越界,且没有染过色
//符合条件处理新位置
DFS(新的位置)
}
}

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int nextP[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
class Solution {
public:
void DFS(vector<vector<int>>& image, int row, int col, int nx, int ny, vector<vector<int>> book, int newColor, int oldColor)
{
image[nx][ny] = newColor;
book[nx][ny] = 1;
//上下左右遍历
for(int i = 0; i < 4; i++)
{
int newx = nx + nextP[i][0];
int newy = ny + nextP[i][1];
if(newx >= row || newx < 0 || newy >= col || newy < 0)
{
continue;
}
if(image[newx][newy] == oldColor && book[newx][newy] == 0)
{
DFS(image, row, col, newx, newy, book, newColor, oldColor);
}
}
}
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {
if(image.empty())
{
return image;
}
int row = image.size();
int col = image[0].size();
int oldColor = image[sr][sc];
vector<vector<int>> book(row, vector<int>(col, 0));
DFS(image, row, col, sr, sc, book, newColor, oldColor);
return image;
}
};

例3 走迷宫

  牛客:
https://www.nowcoder.com/questionTerminal/6276dbbda7094978b0e9ebb183ba37b9

解析

  模型:

1
2
3
4
5
6
DFS()
{
//1.当前位置已走,路径长度+1,并且置为墙表示走过了
//2.走到终点,和最短路径长度比较取最优
//3.遍历四周,如果不是墙且不越界则遍历
}

  但是,这种模型在一张只有极少障碍物的迷宫中要消耗很多的时间,因为我们要让每一个结点都被遍历近10次,这其中会消耗大量时间,并且很多是无用的解。因此我们在其中可以加入动态规划的思想。
  模型:

1
2
3
4
5
DFS()
{
//1.遍历四周,如果不是墙且不越界则进行二次判断
//2.如果本次路径到达这个位置的路径数要小于以往在表中记录的路径数,则将表中最优解进行修改,否则不再遍历此位置。
}

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <string>
#include <vector>
#include <string.h>
//这类迷宫问题不能纯粹使用深度优先搜索,因为要完全遍历一遍很慢,还要加上动态规划算法(剪枝)
//记录到达每个点上的最少步数,如果大于这个步数则不再走这个点,可以剪掉大量冗余无效的走法
using namespace std;
int res[10][10] = { 0 };
vector<string> mess(10, string(10, 0));
int nextP[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
void dfs(int i, int j)
{
//继续向四周遍历
for(int m = 0; m < 4; m++)
{
int newi = i + nextP[m][0];
int newj = j + nextP[m][1];
if(newi < 10 && newi >= 0 && newj < 10 && newj >= 0 && mess[newi][newj] != '#')
{
if(res[newi][newj] == 0 || res[newi][newj] > res[i][j] + 1)
{
res[newi][newj] = res[i][j] + 1;
dfs(newi, newj);
}
}
}
}
int main()
{
while(cin >> mess[0])
{
for(int i = 1; i < 10; i++)
{
cin >> mess[i];
}
dfs(0, 1);
cout << res[9][8] << endl;
memset(res, 0, sizeof(res));
}
}

广度优先搜索(BFS)

  广度优先搜索是以广度为主逐层往外一次遍历,不同于深度优先逐条解进行排列组合,广度优先搜索只会遍历一次所有结点,并且会最先拿到最优解,因此某些情况下广度优先搜索效率优于深度优先搜索。广度优先搜索在迷宫问题的解上效率会更高,在寻找最优解时会更快,因为其不需要列出所有组合的情况。
  广度优先搜索在实现时需要借助队列,每一次将下一层要遍历的结点入队,上一层出队。

例1 员工的重要性

  力扣:
https://leetcode-cn.com/problems/employee-importance/

解析

  将要查找的第一个员工入队,之后每次遍历队中元素将其重要度相加,并且将其下属入队。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
// Employee info
class Employee {
public:
// It's the unique ID of each node.
// unique id of this employee
int id;
// the importance value of this employee
int importance;
// the id of direct subordinates
vector<int> subordinates;
};
*/
class Solution {
public:
int getImportance(vector<Employee*> employees, int id) {
unordered_map<int, Employee*> info;
for(auto& e : employees)
{
info[e->id] = e;
}
queue<int> q;
//将这个员工入队
q.push(id);
int ret = 0;
//广度优先遍历
while(!q.empty())
{
//依次加上队中所有员工的重要度
int curId = q.front();
q.pop();
ret += info[curId]->importance;
//将其下属入队
for(auto& e : info[curId]->subordinates)
{
q.push(e);
}
}
return ret;
}
};

例2 N叉树层序遍历

  力扣:
https://leetcode-cn.com/problems/n-ary-tree-level-order-traversal/submissions/

解析

  建立队列,将根节点入队,创造循环遍历队中每个结点将其孩子入队,但这题要注意要分树的每一行进行遍历,先遍历这一行的结点将其放到结果数组中,再以此遍历下一行的结点。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;

Node() {}

Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public:
vector<vector<int>> levelOrder(Node* root) {
//创造队列
queue<Node*> q;
vector<vector<int>> treeVec;
if(root == nullptr)
{
return treeVec;
}
//放入根结点
q.push(root);
//遍历队中结点
while(!q.empty())
{
//得到这一行的长度
int sz = q.size();
vector<int> rowNode;
//遍历这一行
while(sz--)
{
Node* curNode = q.front();
q.pop();
//将这一行结点的值放到rowNode中
rowNode.push_back(curNode->val);
//将这一行中每个结点的孩子入队
for(auto& chd : curNode->children)
{
q.push(chd);
}
}
treeVec.push_back(rowNode);
}
return treeVec;
}
};

例3 腐烂的橘子

  力扣:
https://leetcode-cn.com/problems/rotting-oranges/

解析

  类似于N叉树的遍历。每一分钟腐烂一层,遍历每一层即可,最后搜索结束后判断是否还有新鲜橘子。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int nextP[4][2] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
queue<pair<int, int>> q;
int row = grid.size();
int col = grid[0].size();
//找出第一批已经坏掉的橘子,入队
for(int i = 0; i < row; i++)
{
for(int j = 0; j < col; j++)
{
if(grid[i][j] == 2)
{
q.push(make_pair(i, j));
}
}
}
//初始状态
int minRet = 0;
while(!q.empty())
{
//跟N叉树遍历一样,这里遍历一层代表要消耗一分钟
int flag = 0;
int sz = q.size();
//遍历这一层
while(sz--)
{
pair<int, int> curPos = q.front();
q.pop();
for(int i = 0; i < 4; i++)
{
int nx = curPos.first + nextP[i][0];
int ny = curPos.second + nextP[i][1];
if(nx >= row || nx < 0 || ny >= col || ny < 0)
{
continue;
}
if(grid[nx][ny] == 1)
{
flag = 1;
grid[nx][ny] = 2;
q.push(make_pair(nx, ny));
}
}
}
if(flag)
{
++minRet;
}
}
for(int i = 0; i < row; i++)
{
for(int j = 0; j < col; j++)
{
if(grid[i][j] == 1)
{
return -1;
}
}
}
return minRet;
}
};

【Cpp】第六章-STL_vector类

发表于 2019-07-24 | 分类于 Cpp
字数统计: 1.7k

vector类

基础介绍

  vector类是STL中另一大容器,它十分类似于一个顺序表,不过经过封装它已经变成了一个可变长度并且拥有各种功能的顺序表,在其内部我们可以通过利用数组进行实现。vector是很常用的容器,因为它支持随机访问,并且尾插和尾删拥有O1的时间复杂度。但是在中间插入时要更高的时间复杂度,最差情况下需要遍历整个数组才能进行插入。它与string的物理与逻辑结构上十分相似,不过它是一个模板类,我们可以在其中存放任意类型的数据。

常用接口

构造函数

1
2
3
4
vector();                                                  //无参构造
vector(size_type n, const value_type &val = value_type()); //构造并初始化n个val
vector(const vector &x); //拷贝构造
vector(InputIterator first, InputIterator last); //使用迭代器进行初始化构造

容量相关

1
2
3
4
5
size();     //获取数据个数
capacity(); //获取容量大小
empty(); //判断是否为空
void resize(size_type n, value_type val = value_type());//改变空间大小,如果大于原有空间用val填充
改变vector的size void reserve(size_type n); //改变vector放入capacity

增删查改

1
2
3
4
5
6
void push_back(const value_type &val);                     //尾插
void pop_back(); //尾删
iterator insert(iterator position, constvalue_type & val); //在position之前插入val
iterator erase(iterator position); //删除position位置的数据
void swap(vector & x); //交换两个vector的数据空间
reference operator[](size_type n); //像数组一样访问

迭代器相关

1
2
3
4
5
6
begin();  //获取第一个数据位置的iterator
end(); //获取最后一个数据的下一个位置的iterator
rbegin(); //获取最后一个数据位置的reverse_iterator
rend(); //获取第一个数据前一个位置的reverse_iterator
cbegin(); //获取第一个数据位置的const_iterator
cend(); //获取最后一个数据的下一个位置的const_iterator

迭代器失效问题

  在容器使用中我们经常会要操作迭代器,很多容器接口中也有提供使用迭代器进行增加删除修改的操作。但是迭代器本身是一个指针,在我们使用接口进行增加或是删除的时候这个迭代器的指向的内容会发生改变甚至是指向非法内存,原因是原来的内存已经被释放,称之为迭代器失效,迭代器失效会导致指向内容改变,从而引起bug甚至是内存越界访问导致程序崩溃。

常见场景

  最为常见的是erase和insert导致的迭代器失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
int a[] = {1, 2, 3, 4};
vector<int> v(a, a + sizeof(a) / sizeof(int));
vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
v.erase(it);
++it;
}
}

  以上这段程序就会引起迭代器失效,在删除最后一个元素是甚至导致内存越界访问,程序崩溃。因此可以总结在利用迭代器删除元素后,删除位置及其之后的所有迭代器都会失效,原因是,之后的所有元素都会前移,之前的迭代器所指向的内容都会发生改变,甚至是指向非法内存。
  同理我们在insert之后由于删除位置及其以后的元素会后移进行变动,因此插入位置及其之后所有的迭代器也都会失效,并且如果有扩容发生还可能发生内存越界访问。
  对于迭代器失效我们要如何避免呢?我们注意看erase的接口可以发现erase返回的是一个迭代器。说明:

1
2
An iterator pointing to the new location of the element that followed the last element erased by the function call. 
This is the container end if the operation erased the last element in the sequence.

  它的返回值是返回在删除位置之后紧接着它的元素的迭代器,这个迭代器保证是有效的,insert的返回值也是一个迭代器,并且是插入位置插入后的元素的迭代器,因此它们的返回值都是保证有效的,所以我们的代码可以改成以下这样就可以避免迭代器失效。

实现

  我们模拟实现一个Vector类,并且实现常用接口,让其达到和vector相同的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#pragma once
#include <assert.h>
#include <memory.h>
template <class T>
class Vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
//构造函数
Vector()
:_start(nullptr)
,_finish(nullptr)
,_endOfStorge(nullptr)
{}
Vector(size_t n, T val)
:_start(nullptr)
,_finish(nullptr)
,_endOfStorge(nullptr)
{
Reserve(n);
for(int i = 0; i < n; i++)
{
*_finish = val;
_finish++;
}
}
//拷贝构造
Vector(const Vector<T>& vec)
:_start(nullptr)
,_finish(nullptr)
,_endOfStorge(nullptr)
{
Resize(vec.Size());
memcpy(_start, vec._start, vec.Size() * sizeof(T));
_finish = _start + vec.Size();
}
//operator=重载
Vector& operator=(Vector<T> vec)
{
Swap(vec);
return *this;
}
//交换
void Swap(Vector<T>& vec)
{
std::swap(_start, vec._start);
std::swap(_finish, vec._finish);
std::swap(_endOfStorge, vec._endOfStorge);
}
//析构函数
~Vector()
{
delete[] _start;
_start = nullptr;
_finish = nullptr;
_endOfStorge = nullptr;
}
//迭代器相关
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
//插入
iterator Insert(iterator pos, const T& val)
{
assert(pos >= _start && pos <= _finish);
//扩容,扩容会导致pos要重新指定位置,因为内存地址变更
if(_finish == _endOfStorge)
{
size_t n = pos - _start;
size_t size = Capacity() == 0 ? 4 : 2 * Capacity();
Reserve(size);
pos = _start + n;
}
iterator it = _finish;
while(it != pos)
{
*it = *(it - 1);
it--;
}
*pos = val;
_finish++;
return pos;
}
//删除
iterator Erase(iterator pos)
{
assert(pos >= _start && pos <= _finish);
iterator it = pos + 1;
while(it != _finish)
{
*(it - 1) = *it;
it++;
}
_finish--;
return pos;
}
//尾插
void Push_back(const T& val)
{
//扩容
if(_endOfStorge == _finish)
{
size_t capacity = Capacity() == 0 ? 4 : 2 * Capacity();
Reserve(capacity);
}
*_finish = val;
_finish++;
}
//尾删
void Pop_back()
{
assert(_finish > _start);
_finish--;
}
//operator[]重载
T& operator[](size_t pos)
{
return _start[pos];
}
const T& operator[](size_t pos) const
{
return _start[pos];
}
//长度
size_t Size() const
{
return _finish - _start;
}
//容量
size_t Capacity() const
{
return _endOfStorge - _start;
}
//重新给容量
void Reserve(size_t capacity)
{
if(capacity > Capacity())
{
size_t size = Size();
//分配新的内存空间
T* newArr = new T[capacity];
if(_start)
{
memcpy(newArr, _start, size * sizeof(T));
}
//销毁原有内存空间
delete[] _start;
//注意这里更新三个指针都要进行更新,因为三个指针都还指向原来的内存空间
_start = newArr;
_finish = _start + size;
_endOfStorge = _start + capacity;
}
}
//重新给长度,空白部分val填充
void Resize(size_t size, T val = T())
{
Reserve(size);
if(Size() < size)
{
T* ptr = _finish;
while(ptr != _start + size)
{
*ptr = val;
ptr++;
}
}
_finish = _start + size;
}
private:
//vecotor的实现与string有所不同
//其底层使用三个迭代器(指针)用来标记头部,尾部和总容量尾部
iterator _start;
iterator _finish;
iterator _endOfStorge;
};

【Cpp】第五章-STL_string类

发表于 2019-07-22 | 分类于 Cpp
字数统计: 2.9k

string类

STL

  STL是Standard Template Library的简称,中文名为是标准模板库,在Cpp中模板是构成泛型编程的基础,我们利用模板可以极大程度地提高我们的代码复用率,但是如果模板要我们现写也有点过于繁琐,不过好在Cpp中为我们写代码方便为我们制作了一套标准地模板库,供我们直接使用十分方便。

STL的版本

  STL发展至今也不是一气呵成的,随着发展和进化,STL一共出现了四大版本。

HP版本

  这个版本是STL的原始版本,由Alexander Stepanov、Meng Lee在惠普实验室完成,是所有STL版本的始祖。并且此版本秉承开源精神,允许任何人免费运用,拷贝,商用,传播,修改这些代码,唯一的条件也只是要求需要像原始版本一样开源使用。

P.J.版本

  这个版本由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不可公开或修改,可读性较差。

RW版本

  这个版本由Rouge Wage公司开发,继承自HP版本,被C++ Builder采用,不能公开或修改,可读性一般。

SGI版本

  这个版本由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版本,被GCC采用,可移植性较好,可公开,修改,贩卖,可读性很高。也是我们学习主要参考的版本。

STL六大组件

  STL中包含六大组件,他们共同组成STL互相协同工作。

容器

  string, vector, list, deque, map, set, multimap, multiset。

配接器

  stack, queue, priority_queue

算法

  find, swap, reverse, sort, merge...

空间适配器

  allocator

迭代器

  iterator, const_iterator, reverse_iterator, const_reverse_iterator

仿函数

  greater, less...

  STL在日常编程中无论是笔试还是项目都十分常用,必须多用多练,并且自己实现一遍才能熟练掌握。STL(包扩Cpp绝大部分库)学习可分为三个层次:

  1、熟用STL
  2、了解泛型技术d的内涵与STL的学理乃至实作
  3、扩充STL
  总结就是能用,明理,能扩展。

string类

  string类时STL中专门用于字符串处理的容器。在C语言中我们利用字符数组或字符指针来构成字符串,所有字符串使用十分不方便,库中为字符串提供的接口也并不便于使用,于是在Cpp中有了string模板类,这个容器可以帮助我们更加方便的使用字符串,并且帮助我们封装了很多字符串相关的常用接口。

常用接口

构造函数

  string中提供了各种构造函数帮助我们构造字符串。

1
2
3
4
5
string(); //构造空的string类对象,即空字符串
string(const char *s);// 用C-string来构造string类对象
string(size_t n, char c);//string类对象中包含n个字符c
string(const string &s);//拷贝构造函数
string(const string &s, size_t n);//用s中的前n个字符构造新的string类对象 return 0;

容量相关接口

1
2
3
4
5
6
7
8
size_t size() const;			  // 返回字符串有效字符长度
size_t length() const; // 返回字符串有效字符长度
size_t capacity() const; // 返回空间总大小
bool empty() const; // 检测字符串释放为空串,是返回true,否则返回false
void clear(); //清空有效字符
void resize(size_t n, char c); // 将有效字符的个数该成n个,多出的空间用字符c填充
void resize(size_t n); // 将有效字符的个数改成n个,多出的空间用0填充
void reserve(size_t res_arg = 0); // 为字符串预留空间

访问相关接口

1
2
char& operator[] (size_t pos); 	  	    //返回pos位置的字符,非const string类对象调用
const char& operator[] (size_t pos); //const返回pos位置的字符,const string类对象调用

修改相关接口

1
2
3
4
5
6
7
8
9
10
void push_back(char c);							//在字符串后尾插字符c
string& append (const char* s); //在字符串后追加一个字符串
string& operator+=(const string& str); //在字符串后追加字符串str
string& operator+=(const char* s); //在字符串后追加字符串
string& operator+=(char c); //在字符串后追加字符c
const char* c_str()const; //返回C格式字符串
size_t find (char c, size_t pos = 0) const; //从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
size_t rfind(char c, size_t pos = npos); //从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
string substr(size_t pos = 0, size_t n= npos); //const在str中从pos位置开始,截取n个字符,然后将其返回
string& erase (size_t pos = 0, size_t len = npos); //从pos位置起删除串中npos个字符

迭代器相关

  迭代器十分类似于指针,我们可以将其等同于一个自定义类型的指针,用它我们可以完成容器内的遍历,增加,删除等操作,STL中容器的很多功能也为迭代器设计了很多接口,其中最为常用的还是取到一个容器的迭代器。

1
2
3
4
iterator begin();					//取到头部迭代器
const_iterator begin() const; //取到头部常迭代器
iterator end(); //取到尾部迭代器
const_iterator end() const; //取到尾部常迭代器

其他接口

1
2
3
4
5

string operator+ (const string& lhs, const string& rhs); //在lhs串后拼接rhs串
istream& operator>> (istream& is, string& str); //输入运算符重载
istream& getline (istream& is, string& str); //获取一行字符串
relational operators //大小比较

综合运用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "123456";//单参构造的隐式类型转换 + 拷贝构造
for(int i = 0; i < str.size(); i++)//size()取出长度
{
cout << str[i] << " ";//operator[]重载的运用
}
cout << endl;
str.append("abc");//append()接口
str.push_back('d');//push_back接口使用
str += "efg";//operator += 重载使用
//迭代器的应用
string::iterator it = str.begin();
while(it != str.end())
{
cout << * it << " ";
it++;
}
}


1 2 3 4 5 6
1 2 3 4 5 6 a b c d e f g

实现

  学习STL要熟用,明理,能扩展,那么第二部明理我们就要自己实现封装一个string类。根据库中string常用接口我们也实现其基本功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
#include <assert.h>
#include <string.h>
#include <cstdio>
#include <algorithm>
class String
{
friend std::ostream &operator<<(std::ostream &os, String str);
public:
//迭代器
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
const_iterator begin() const
{
return _str;
}
iterator end()
{
return _str+_size;
}
const_iterator end() const
{
return _str+_size;
}
static size_t npos;
//构造函数
String(const char* str = "")//要进行深拷贝
:_str(nullptr)
,_capacity(0)
{
_size = strlen(str);
//重新给容量Reserve()
Reserve(_size);
//strcpy()拷贝给成员变量
strcpy(_str, str);
}
//拷贝构造,要使用深拷贝
//所谓深拷贝就是创立独立的内存空间并将目标对象中的值拷贝过来
//而不是单纯的让指针等于目标拷贝对象中的指针
//注意:拷贝构造和operator=重载都是不拷贝容量大小的
//传统写法:创建新的独立内存,销毁原来的内存空间,更新_size, _capacity的值
//String(const String& str)
// :_str(nullptr)
// ,_size(0)
// ,_capacity(0)
//{
// Resize(str.Size());
// strcpy(_str, str._str);
//}
////operator=重载和拷贝构造类似,先用传统写法实现
//String& operator=(const String& str)
//{
// if(this != &str)
// {
// Resize(str.Size());
// strcpy(_str, str._str);
// }
//}
//现代写法,另外创建对象让其等于要拷贝的对象,交换两个对象即可
String(const String& str)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
String temp(str._str);
Swap(temp);
}
//现代写法,利用拷贝构造函数创建临时对象,交换两个对象,临时对象在函数结束时也会自动释放
String& operator=(String str)
{
Swap(str);
}
//交换两个字符串,浅拷贝,不另申请内存空间
void Swap(String& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
//析构函数
~String()
{
if(_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
//返回_size
size_t Size() const
{
return _size;
}
//返回_capacity
size_t Capacity() const
{
return _capacity;
}
//在某个下标插入一个字符
void Insert(size_t pos, char ch)
{
assert(pos <= _size);
//容量满了,扩容
if(_size == _capacity)
{
Reserve(2 * _capacity);
}
for(int i = _size; i > pos; i--)
{
_str[i] = _str[i - 1];
}
_str[pos] = ch;
_size++;
_str[_size] = '\0';
}
//在某个下标处插入一个字符串
void Insert(size_t pos, const char* str)
{
assert(pos <= _size);
int len = strlen(str);
//容量不够扩容
if(_size + len > _capacity)
{
Reserve(len + _size);
}
for(int i = len + _size - 1; i > pos + len - 1; i--)
{
_str[i] = _str[i - len];
}
for(int i = pos; i < pos + len; i++)
{
_str[i] = str[i - pos];
}
_size += len;
}
//+=运算符重载
String& operator+=(char ch)
{
Push_back(ch);
}
String& operator+=(const char* str)
{
Append(str);
}
//删除pos下标开始的npos个字符
void Erase(size_t pos, size_t npos)
{
assert(pos < _size);
for(int i = pos; i < _size - npos; i++)
{
_str[i] = _str[i + npos];
}
_size -= npos;
_str[_size] = '\0';
}
//从pos开始找第一个字符为ch返回其下标
size_t Find(const char ch, size_t pos = 0)
{
assert(pos < _size);
for(size_t i = pos; i < _size; i++)
{
if(_str[i] == ch)
{
return i;
}
}
return npos;
}
//从pos开始找第一个子串为str返回其下标
size_t Find(const char* str, size_t pos = 0)
{
assert(pos < _size);
for(size_t i = pos; i < _size; i++)
{
if(_str[i] == str[0])
{
int j = i;
while (j < i + strlen(str) && _str[j] != '\0')
{
if (_str[j] != str[j - i])
{
break;
}
j++;
}
//子串遍历完毕,子串与要查找的串完全匹配
if (j == i + strlen(str))
{
return i;
}
//主串遇到结尾,长度不够不用继续查找了
else if(_str[j] == '\0')
{
break;
}
//其他情况本次子串与要查找的串匹配不上,继续下一次子串查找
}
}
return npos;
}
//在尾部插入字符
void Push_back(char ch)
{
Insert(_size, ch);
}
//字符串拼接
void Append(const char* str)
{
Insert(_size, str);
}
//重新给容量,并且要求容量永远为8的整数倍
void Reserve(size_t n)
{
if(n > _capacity || (n == 0 && _capacity == 0))
{
size_t newCapacity = n;
if(newCapacity % 8 != 0)
{
newCapacity = (((newCapacity / 8) + 1) * 8);
}
else
{
newCapacity += 8;
}
char* newStr = new char[newCapacity];
if(newStr && _str)
{
strcpy(newStr, _str);
}
//释放旧空间
delete[] _str;
_str = newStr;
_capacity = newCapacity - 1;
return;
}
}
void Resize(size_t size, char ch = '\0')
{
if(size < _size)
{
_size = size;
_str[_size] = '\0';
}
else
{
Reserve(size);
for(size_t i = _size; i < size; i++)
{
_str[i] = ch;
}
_size = size;
_str[_size] = '\0';
}
}
//>运算符重载
bool operator>(const String& str)
{
if(strcmp(_str, str._str) > 0)
{
return true;
}
return false;
}
bool operator==(const String& str)
{
if(strcmp(_str, str._str) == 0)
{
return true;
}
return false;
}
bool operator>=(const String& str)
{
if(*this > str || *this == str)
{
return true;
}
return false;
}
bool operator<(const String& str)
{
if(*this >= str)
{
return false;
}
return true;
}
bool operator<=(const String& str)
{
if(*this < str || *this == str)
{
return true;
}
return false;
}
bool operator!=(const String& str)
{
if(*this == str)
{
return false;
}
return true;
}
//+运算符重载
String operator+(char ch)
{
String temp(*this);
temp.Push_back(ch);
return temp;
}
String operator+(const char* str)
{
String temp(*this);
temp.Append(str);
return temp;
}
//取类中的字符串
char* c_str()
{
return _str;
}
//operator[]重载
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//operator[]重载
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
std::ostream& operator<<(std::ostream& os, String str)
{
os << str._str;
return os;
}
size_t String::npos = -1;

【算法】第一章-动态规划

发表于 2019-07-20 | 分类于 算法
字数统计: 2.2k

第一章-动态规划

动态规划求解模式

  动态规划具备了一下三个特点:
  1、把原来的问题分解成了几个相似子问题
  2、所有的子问题只需要解决一次
  3、储存子问题的解
  从以下四个角度考虑:
  1、初始状态定义
  2、状态间转移方程
  3、状态的初始化
  4、返回结果
  解决问题主要适用于:查找最优解,最大值/最小值,可不可行,是不是,方案个数

例1 斐波那契数列

  牛客网:
https://www.nowcoder.com/questionTerminal/c6c7742f5ba7442aada113136ddea0c3

  动态规划解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int Fibonacci(int n) {
vector<int> ret(n + 1, 0);
//初始状态
ret[1] = ret[2] = 1;
//递推公式
//ret[i] = ret [i - 1] + ret[i - 2];
for(int i = 3; i <= n; i++)
{
ret[i] = ret[i - 1] + ret[i - 2];
}
return ret[n];
}
};

例2 变态青蛙跳台阶

  牛客网:
https://www.nowcoder.com/questionTerminal/22243d016f6b47f2a6928b4313c85387

问题分析

定义状态

  跳上i级台阶的方法数

状态转移方程

  分解:
  一次跳1级台阶:1, F(i - 1)
  一次跳2级台阶:2, F(i - 2)
  ……
  推得状态转移方程:
  F(i) = F(i - 1) + F(i - 2) + … + F(1)
  F(i - 1) = F(i - 2) + F(i - 3) + … + F(1)
  F(i) = F(i - 1) + F(i - 1) = 2 * F(i - 1);

初始状态和最终状态

  F(1) = 1,求F(n)。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int jumpFloorII(int number) {
//初始状态
int f1 = 1;
//状态转移方程:F(i) = 2 * F(i - 1)
for(int i = 2; i <= number; i++)
{
f1 = 2 * f1;
}
return f1;
}
};

优化

  根据状态转移方程我们可以得知,F(n) = 2 ^ (n - 1);所以可以得到优化:

1
2
3
4
5
6
class Solution {
public:
int jumpFloorII(int number) {
return (1 << (number - 1));
}
};

例3 求连续最大子数组的和

  牛客网:

https://www.nowcoder.com/questionTerminal/459bd355da1549fa8a49e350bf3df484

分析

定义状态

  到此项为止前i项的连续子序列的最大和

状态转移方程

  F(i) = max(F(i - 1) + a[i], a[i]);

初始状态和最终状态

  初始状态F[0] = a[0]。
  最终结果max(F[i])。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
if(array.empty())
{
return 0;
}
vector<int> ret(array.size(), 0);
//初始状态
ret[0] = array[0];
//状态转移
for(int i = 1; i < array.size(); i++)
{
ret[i] = max(array[i] + ret[i - 1], array[i]);
}
//求max(F[i])
int maxNum = ret[0];
for(int i = 0; i < ret.size(); i++)
{
maxNum = max(maxNum, ret[i]);
}
return maxNum;
}
};

例4 word-break(字符串分割)

  牛客网:
https://www.nowcoder.com/questionTerminal/5f3b7bf611764c8ba7868f3ed40d6b2c

分析

定义状态

  F(i):前i个字符能否被分割。

状态转移方程

  F(i): F(i)能否被分割取决于前j项能否被分割和j + 1到第i项能否被分割。(此处j取0 ~ i - 1)
  得递推方程F(i) : F(j) && (j + 1 ~ i)能否被分割

初始状态和最终状态

  初始状态:F(0) = true。
  最终状态F(n)。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
bool wordBreak(string s, unordered_set<string> &dict) {
if(s.empty() || dict.empty())
{
return false;
}
vector<bool> Fn(s.size() + 1, false);
//初始状态
Fn[0] = true;
for(int i = 1; i <= s.size(); i++)
{
//状态转移
//F(i) = F(j) && (j + 1 ~ i), (0 <= j < i)能否被分割
for(int j = i - 1; j >= 0; j--)
{
if(Fn[j] && dict.find(s.substr(j, i - j)) != dict.end())
{
Fn[i] = true;
break;
}
}
}
//最终状态
return Fn[s.size()];
}
};

例5 triangle(三角矩阵最短路)

  牛客网:
https://www.nowcoder.com/questionTerminal/2b7995aa4f7949d99674d975489cb7da

分析

定义状态

  F[i][j]: 从(0, 0)到(i, j)的最短路径和。

状态转移方程

  一般情况下,走到每个位置的最短路径都可以看作是上一行相邻两个位置的最短路径+这个位置的路径长度,数学描述:F[i][j]:min(F[i - 1][j], F[i - 1][j - 1]) + a[i][j]。
  但要考虑边界状态:当在矩阵边缘时只有一种选择的情况,即F[i][0] = F[i - 1][0];F[i][i] = F[i - 1][i - 1]。

初始状态和最终状态

  初始状态:F[0][0] = a[0][0]。
  最终状态:min(F[n - 1][j]),即最后一行中的最小值。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
int minimumTotal(vector<vector<int> > &triangle) {
if(triangle.empty())
{
return 0;
}
//初始状态
vector<vector<int>> ret(triangle);
//状态转移
int row = ret.size();
for(int i = 1; i < row; ++i)
{
ret[i][0] = ret[i - 1][0] + triangle[i][0];
}
for(int i = 1; i < row; ++i)
{
ret[i][i] = ret[i - 1][i - 1] + triangle[i][i];
}
for(int i = 1; i < row; ++i)
{
for(int j = 1; j < i; ++j)
{
ret[i][j] = min(ret[i - 1][j], ret[i - 1][j - 1]) + triangle[i][j];
}
}
//最终状态
int minSum = ret[row - 1][0];
for(int i = 1; i < row; i++)
{
minSum = min(minSum, ret[row - 1][i]);
}
return minSum;
}
};

例6 求路径数量

牛客网:
https://www.nowcoder.com/questionTerminal/3cdf08dd4e974260921b712f0a5c8752

分析

状态定义

  F[i][j]:从左上角到达下标(i, j)的路径总数

状态转移方程

  if(a[i][j] == 1) F[i][j] = 0;
  if(a[i][j] == 0) F[i][j] = F[i][j - 1] + F[i - 1][j]。

初始状态和最终状态

  初始状态:如果第一行第一列有障碍物表示这条路走不通则路径数置0,否则只有一条路径,置一if(a[0][j]) == 1) F[0][j] = 0,if(a[i][0] == 1) F[i][0] = 0,这种情况下这条路走不通。
  最终状态:F[i][j]。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int> > &obstacleGrid) {
if(obstacleGrid.empty())
{
return 0;
}
if(obstacleGrid[0][0] == 1)
{
return 0;
}
int row = obstacleGrid.size();
int col = obstacleGrid[0].size();
vector<vector<int>> pathNum(row, vector<int>(col, 0));
for(int i = 0; i < row; i++)
{
if(obstacleGrid[i][0] == 1)
{
break;
}
else
{
pathNum[i][0] = 1;
}
}
for(int i = 0; i < col; i++)
{
if(obstacleGrid[0][i] == 1)
{
break;
}
else
{
pathNum[0][i] = 1;
}
}
for(int i = 1; i < row; i++)
{
for(int j = 1; j < col; j++)
{
if(obstacleGrid[i][j] == 1)
{
pathNum[i][j] = 0;
}
else
{
pathNum[i][j] = pathNum[i][j - 1] + pathNum[i - 1][j];
}
}
}
return pathNum[row - 1][col - 1];
}
};

例7 背包问题

  lintcode:
https://www.lintcode.com/problem/backpack-ii/description

分析

  背包问题是一类问题,主要是求解约束条件下的最优解问题。

状态定义

  F[i][j]:前i个商品,包的重量为j的最大价值。定义二维数组遍历所有商品,和到当前商品情况下背包容量从0 - max的所有情况下的最大值求解,遍历到最后则可得到n个全部商品情况下背包最大容量max下背包可拿的最大价值F[n][max]。

状态转移方程

  当前商品空间大于包能承受的总空间,放不下直接跳过。数学描述:if(w[i] > j) F[i][j] = F[i - 1][j]。
  当前商品空间小于等于包能承受总空间,可以选择放可以选择不放,如果不放入,则F[i][j] = F[i - 1][j];如果放入,则当前重量等于遍历到的上一个商品并且空间还有w[i]这么大的时候的价值+本商品的价值,最大价值F[i][j] = F[i - 1][j - w[i]] + v[i],选择放入或不放入商品情况中的最大值作为最优解。数学描述:if(w[i] <= j) max(F[i - 1][j], F[i - 1][j - w[i]] + v[i]);

初始状态和最终状态

  初始状态:没有商品时,F[0][j] = 0,最大容量为0时F[i][0] = 0。
  最终状态:遍历完全部n个商品,并且背包总容量为最大值m时得到最大价值F[n][m]。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public:
/**
* @param m: An integer m denotes the size of a backpack
* @param A: Given n items with size A[i]
* @param V: Given n items with value V[i]
* @return: The maximum value
*/
int backPackII(int m, vector<int> &A, vector<int> &V) {
if(A.empty())
{
return 0;
}
int row = A.size();
vector<vector<int>> maxValue(row + 1, vector<int>(m + 1, 0));
for(int i = 1; i <= row; ++i)
{
for(int j = 1; j <= m; ++j)
{
if(A[i - 1] > j)
{
maxValue[i][j] = maxValue[i - 1][j];
}
else
{
maxValue[i][j] = max(maxValue[i - 1][j], maxValue[i - 1][j - A[i - 1]] + V[i - 1]);
}
}
}
return maxValue[row][m];
}
};

【Cpp】第四章-模板初阶

发表于 2019-07-15 | 分类于 Cpp
字数统计: 1.7k

模板初阶

泛型编程

  在我们进行大型程序的编写时往往会遇到一类问题,同一个函数或类我们希望多种类型数据传入时都能完成类似或者相同的功能,但是在C语言中我们很难做到这一点因为我们往往在换了一个数据类型后就要重新写一遍函数,这样耽误我们大量的时间,呢么有没有一种语法在Cpp中能够使让我们的代码成为一种模板,不同的数据类型传入也依然能够执行类似的功能呢?

  正所谓世界是由懒人创造的,于是在C++中引入了模板这一概念,同时也正得益与此使我们可以做到泛型编程,是我们的代码极大程度的可以复用。

模板

  模板是实现泛型编程的基础,其中又可细分为函数模板和类模板。

函数模板

  函数模板是生成一个家族的函数,其中我们可以使用任意类型的参数进行传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;
//定义函数模板,T为任意类型
//class 也可替换为 typename
template<class T>
void Swap(T& a, T& b)
{
T t = a;
a = b;
b = t;
}

int main()
{
int a = 4, b = 5;
double c = 6, d = 7;
Swap(a, b);
Swap(c, d);
cout << a << " " << b << endl;
cout << c << " " << d << endl;
}


5 4
7 6

原理

  函数模板在定义完之后并不会直接生成函数,而是在调用时会根据传参类型进行推演,在上面的例子中我们传入了int类型的参数因此在推演时会将T转换为int再进行调用,但有时我们的调用如果出现了让编译器无法推演的情况就会导致报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
using namespace std;
template<class T>
void Swap(T& a, T& b)
{
T t = a;
a = b;
b = t;
}

int main()
{
int a = 4, b = 5;
double c = 6, d = 7;
Swap(a, d);
Swap(c, b);
cout << a << " " << b << endl;
cout << c << " " << d << endl;
}


.\test.cpp: In function 'int main()':
.\test.cpp:15:14: error: no matching function for call to 'Swap(int&, double&)'
Swap(a, d);
^
.\test.cpp:4:6: note: candidate: template<class T> void Swap(T&, T&)
void Swap(T& a, T& b)
^~~~
.\test.cpp:4:6: note: template argument deduction/substitution failed:
.\test.cpp:15:14: note: deduced conflicting types for parameter 'T' ('int' and 'double')
Swap(a, d);
^
.\test.cpp:16:14: error: no matching function for call to 'Swap(double&, int&)'
Swap(c, b);
^
.\test.cpp:4:6: note: candidate: template<class T> void Swap(T&, T&)
void Swap(T& a, T& b)
^~~~
.\test.cpp:4:6: note: template argument deduction/substitution failed:
.\test.cpp:16:14: note: deduced conflicting types for parameter 'T' ('double' and 'int')
Swap(c, b);

  我们会发现在这样只有一个模板参数T进行多类型传入就会混淆编译器,导致报错,因此我们可以定义多个模板参数,也可以进行强转使参数类型唯一,不过这里要提到另一种可以让编译器推演出我们想要的函数的方式,显示实例化。

显示实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;
template<class T>
T Add(T a, T b)
{
return a + b;
}

int main()
{
int ret = Add<int>(1, 2);
cout << ret << endl;
}

3

  像是这样的情况我们就利用显示实例化给我们的函数模板制定了实例化类型,同时如果类型不匹配,编译器会进行隐式类型转换,如果转换不成功则会报错。

函数模板匹配原则

  函数模板可以与非模板函数重名,此时会构成类似于函数重载的情况,并且在函数调用时如果出现重名的函数模板和非模板函数都可以构成匹配则会优先调用非模板函数,而不会对函数模板进行实例化,除非我们利用显示实例化指定必须调用模板函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;
template<class T>
T Add(T a, T b)
{
cout << "template function" << endl;
return a + b;
}

int Add(int a, int b)
{
cout << "simple function" << endl;
return a + b;
}
int main()
{
int ret = Add(1, 2);//调用普通函数
ret = Add<int>(1, 2);//调用模板函数
cout << ret << endl;
}



simple function
template function
3

  但是如果模板函数此时可以提供更好的适配性,则会优先调用模板函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;
template<class T1, class T2>
T1 Add(T1 a, T2 b)
{
cout << "template function" << endl;
return a + b;
}

int Add(int a, int b)
{
cout << "simple function" << endl;
return a + b;
}
int main()
{
int ret = Add(1, 2.0);//优先调用函数模板
ret = Add<int, double>(1, 2.0);
cout << ret << endl;
}



template function
template function
3

类模板

  类模板与函数模板类似,是使用一个模板参数来构造整个类,并且原理也与函数模板类似,只有在类构造对象时才会推演模板参数进行实例化,实例化出我们想要的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <iostream>
#include <assert.h>
using namespace std;
template<class T>
class Vector
{
public:
Vector(size_t capacity = 10)
:_pData(new T[capacity])
,_size(0)
,_capacity(capacity)
{

}
~Vector();
//返回size
size_t Size()
{
return _size;
}
//尾插
void Push_back(const T& data)
{
//检查扩容
if(_size >= _capacity)
{//扩容
Reserve(2 * _capacity);
}
_pData[_size] = data;
_size++;
}
//尾删
void Pop_back()
{
_size--;
}
//改变容量
void Reserve(int capacity)
{
if(capacity <= _capacity)
{
return;
}
T* newPData = new T[capacity];
for(int i = 0; i < _size; i++)
{
newPData[i] = _pData[i];
}
_pData = newPData;
_capacity = capacity;
}
T operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
//在类外进行函数声明时要加上模板参数
//同时要注意Vector不是一个类,实例化后Vector<T>才是一个类
template<class T>
Vector<T>::~Vector()
{
if(_pData)
{
delete[] _pData;
}
}
int main()
{
Vector<int> arr;
arr.Push_back(1);
arr.Push_back(2);
for(int i = 0; i < arr.Size(); i++)
{
cout << arr[i] << endl;
}
}


1
2

  这个例子我们模拟简单实现了一个vector模板类,并且使用实例化进行使用,类模板与函数模板不同的是类模板往往无法推演出模板参数类型因此需要我们显示实例化使其实例化为一个具体的类才可以进行使用。

【Cpp】第三章-内存管理

发表于 2019-06-11 | 分类于 Cpp
字数统计: 2.6k

内存管理

C++内存管理

  在C语言中,我们想要动态分配内存空间需要使用到malloc,calloc,realloc函数,在C++中我们同样有动态进行内存管理的方式,并且与C语言中的内存管理有着一些区别。

new/delete

  在C++中我们使用new进行内存的申请,用delete进行内存的释放。他们的使用比malloc和free更加简单方便。

内置类型的内存分配与释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
int main()
{
int* a = new int;
//等同于int* a = (int*)malloc(sizeof(int));
int* b = new int[10];
//等同于int* b = (int*)malloc(sizeof(int) * 10);
int* c = new int(10);
//new还可以进行内置类型的初始化
cout << *c << endl;
delete a;
//等同于free(a);
delete[] b;//对于多个变量的空间释放要用delete[]
//等同于free(b);
delete c;
//等同于free(c);
}


[misaki@localhost 第三章-内存管理]$ ./New
10

  new和malloc一样会在堆上开辟空间同时需要我们手动进行内存的释放,但是new的写法更加简单易于理解同时我们还可以对单个申请的变量进行初始化。

自定义类型的内存分配与释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>                                 
#include <stdlib.h>
using namespace std;
class Stu
{
public:
Stu()
{
cout << "default building" << endl;
};
Stu(int num, string name):_num(num), _name(name)
{
cout << "custom building" << endl;
}
~Stu()
{
cout << "destroying" << endl;
}
private:
int _num;
string _name;
};
int main()
{
cout << "malloc:" << endl;
Stu* a = (Stu*)malloc(sizeof(Stu));
cout << "new:" << endl;
Stu* b = new Stu(1, "张三");
cout << "malloc:" << endl;
Stu* c = (Stu*)malloc(sizeof(Stu) * 5);
cout << "new:" << endl;
Stu* d = new Stu[5];
cout << "free:" << endl;
free(a);
cout << "delete:" << endl;
delete b;
cout << "free:" << endl;
free(c);
cout << "delete:" << endl;
delete[] d;
}


[misaki@localhost 第三章-内存管理]$ ./New
malloc:
new:
custom building
malloc:
new:
default building
default building
default building
default building
default building
free:
delete:
destroying
free:
delete:
destroying
destroying
destroying
destroying
destroying

  以上这段代码我分别使用malloc和new对自定义类型进行内存分配和释放,我们可以发现new不但可以在分配内存的时候手动调用指定的构造函数还会在分配多个对象的空间时自动调用默认构造函数,delete也会自动调用析构函数,而malloc和free却做不到这一点。因此可以理解为malloc和free分配出来的只不过是一个和类一样大小的空间,并不能称作是一个对象,而new和delete分配出来的才能被成为对象。

new和delete实现原理

  new和delete在C++中其实被定义为两个运算符,我们在使用这两个运算符的时候它会在底层调用全局函数operator new和operator delete。

operator new/operator delete

  我们首先看下这两个函数在底层的实现源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse );
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}

  从源码中能看出的是operator new和operator delete在底层也是利用malloc和free分配内存的,因此可以说new和delete不过是malloc和free的一层封装。因此在某些情况下,我们想要用独特的方式给一个类分配内存空间的时候我们就可以重新重载这两个运算符来达到我们的目的。基于这个原理如果有某些类需要特殊的内存分配方式我们可以对其进行运算符的重载。

实现原理

内置类型

  对于内置类型来说new和malloc,delete和free的功能一致,不同的是new[]和delete[]才能分配多个连续的空间。

自定义类型

单个元素空间分配

  1、new:

    1)首先调用operator new为对象分配空间。

    2)调用对象的构造函数对对象进行初始化。

  2、delete:

    1)调用对象的析构函数进行对象中资源的空间清理。

    2)调用operator delete释放对象的空间。

多个元素空间分配

  1、new[]:

    1)调用operator new[],在operator new[]中调用operator new完成N个对象的空间的分配。

    2)调用构造函数N次完成N个对象的初始化。

  2、delete[]:

    1)调用析构函数N次完成N个对象资源的清理。

    2)调用operator delete[],在operator delete[]中调用operator delete完成N个对象的空间的释放。

定位new表达式

  当我们用malloc或者其他方式分配了一块和某个类一样大小的空间给,并用某个指针去指向这块空间。但是问题在于这块空间并未执行构造函数因此并不能称为对象。因此定位new表达式就是为了帮助我们对一块已经分配好的空间执行构造函数使之成为对象的一个方式。

用法

  new (place_address) type(initializer-list)。

  place_address为指向某一块空间的指针,type为自定义类型,initializer-list为参数列表。

例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>                                 
#include <stdlib.h>
using namespace std;
class Stu
{
public:
Stu()
{
cout << "default building" << endl;
};
Stu(int num, string name):_num(num), _name(name)
{
cout << "custom building" << endl;
}
~Stu()
{
cout << "destroying" << endl;
}
private:
int _num;
string _name;
};
int main()
{
Stu* p = (Stu*)malloc(sizeof(Stu));
cout << "定位new表达式:" << endl;
new(p) Stu(1,"张三");
delete p;
}


[misaki@localhost 第三章-内存管理]$ ./New
定位new表达式:
custom building
destroying

  这样我们就用定位new表达式给已经分配好的空间调用了构造函数。

常见问题

new和malloc的异同

相同

  1、new和delete都在堆上进行空间的申请。

  2、都需要手动释放空间。

不同

  1、malloc和free是函数而new和delete是运算符。

  2、new可以在分配空间的时候进行初始化。

  3、malloc返回值是void*需要强转,new会直接返回与分配空间类型一样的类型指针。

  4、malloc需要手动计算分配空间大小在进行传入,而new只需要类型和元素个数,空间大小会自动计算。

  5、new在给自定义类型分配空间的时候会自动调用其构造函数,delete会自动调用其析构函数。

  6、malloc申请空间失败会返回NULL,new会抛异常。

  7、new和delete是malloc和free的一层封装,因此效率会低一些。

写一个只能在堆上创建对象的类

思路

  1、将构造函数,赋值构造函数全部封装为私有,不允许外部直接调用构造。

  2、单独写一个静态函数提供在堆上创建对象的接口。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;
class HeapOnly
{
public:
//开放构造接口
static HeapOnly* Create()
{
cout << "create int heap" << endl;
return new HeapOnly();
}
~HeapOnly()
{
cout << "destorying" << endl;
}
private:
//构造函数私有化
HeapOnly(){}
HeapOnly(const HeapOnly&){}
};
int main()
{
HeapOnly* heapOnly = HeapOnly::Create();
delete heapOnly;
}


[misaki@localhost 第三章-内存管理]$ ./HeapOnlyClass
create int heap
destorying

写一个只能在栈上创建对象的类

思路一

  1、将构造函数,赋值构造函数全部封装为私有,不允许外部直接调用构造。

  2、单独写一个静态函数提供在栈上创建对象的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;
class StackOnly
{
public:
static StackOnly Create()
{
//创建匿名对象
return StackOnly();
}
static StackOnly a;
~StackOnly()
{
cout << "destorying" << endl;
}
private:
StackOnly()
{
cout << "create in stack" << endl;
}
StackOnly(const StackOnly&){}
};
int main()
{
StackOnly::a = StackOnly::Create();
}


[misaki@localhost 第三章-内存管理]$ ./StackOnlyClass
create in stack
destorying

思路二

  1、直接将operator new和operator delete重载并定义为私有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>                     
using namespace std;
class StackOnly
{
public:
StackOnly()
{
cout << "create in stack" << endl;
}
~StackOnly()
{
cout << "destorying" << endl;
}
private:
void* operator new(size_t size){}
void operator delete(void* p){}
};


int main()
{
StackOnly p;
}

实现单例模式

  单例模式是一种设计模式,在实战中经常会用到。其意思是创建一个类这个类只能唯一的创建一个对象,如果之后还想用这个类创建新对象的时候都会返回最开始创建的呢个对象。

  要实现这一点有两种思路,分别成为懒汉模式和饿汉模式。

饿汉模式

  饿汉模式是在程序启动时就夹在所有需要资源的设计模式,用这种思想实现单例模式时需要在程序一开始就直接声明对象,需要适用对象就返回对象即可。但是坏处是程序启动时会消耗时间可能会造成程序启动缓慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
class Singleton1
{
public:
//外留接口
static Singleton1* GetInstence()
{
return &_singleton;
}
private:
//防拷贝,禁用构造,拷贝构造和赋值
Singleton1()
{

}
Singleton1(const Singleton1&);
Singleton1& operator=(const Singleton1&);
static Singleton1 _singleton;
};
Singleton1 Singleton1::_singleton;

懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Singleton2
{
public:
static Singleton2* GetInstence()
{
//双重判断避免不必要的锁竞争
if(_pSingleton == nullptr)
{
//为了线程安全需要加锁
_mtx.lock();
if (_pSingleton == nullptr)
{
_pSingleton = new Singleton2;
}
_mtx.unlock();
}
return _pSingleton;
}
private:
Singleton2(){}
Singleton2(const Singleton2&);
Singleton2& operator=(const Singleton2&);
static std::mutex _mtx;
static Singleton2* _pSingleton;
};
std::mutex Singleton2::_mtx;
Singleton2* Singleton2::_pSingleton = nullptr;

  懒汉模式就是在第一次使用时才会创建对象,因此我们需要对变量进行判断考虑到线程安全我们需要对其加锁。懒汉模式的坏处是可能造成程序运行卡顿。

1…345…9
MisakiFx

MisakiFx

Hard working or giving up!!!

86 日志
10 分类
64 标签
GitHub E-Mail 网易云音乐 CSDN

© 2020 MisakiFx
我的网站的访客数:
|
主题 — NexT.Pisces v5.1.4
博客全站共273.4k字