C++的学习心得和知识总结 第十四章(完)

本章内容:OOP思想的应用,具体代码输出

文章目录

  • 第一题:求迷宫路径(深度优先遍历)
  • 第二题:求迷宫的最短路径(广度优先遍历)
  • 第三题:大数的加减法
  • 第四题:海量数据Top K系列问题和查重问题
  • 第五题:海量数据Top K
  • 第六题:海量数据Top K系列问题和查重问题的综合应用

第一题:求迷宫路径(深度优先遍历)

问题描述如下:
C++的学习心得和知识总结 第十四章(完)_第1张图片
在使用OOP思想解决问题的时候,需要从具体的问题场景中。抽象出来哪些实体,在计算机上描述这个实体,那也就是 。根据实体间的关系,也就得出了类与类之间的关系。

分析:迷宫行列,由人给出。所以说这个描述迷宫的二维数组是动态开辟出来的。迷宫整个地图可以看做是一个类(行、列、以及迷宫节点类型的二级指针:动态开辟二维数组)
迷宫是由一个个的具体位置节点组成,这每一个节点可以抽象成一个类(属性:横坐标、纵坐标、节点值、一个方向数组,表示这个节点四个方向的可走性);

接下来的问题就是:如何深度搜索一个迷宫路径?
答:如果使用非递归来实现深度搜索,那么肯定是需要一个栈了。如果当前节点可以走(其值为0),然后这个节点就可以放入到这个栈中了。然后开始查看栈顶元素,查看其4个方向的可走性,然后确定它是留在(保存最终的路径结果)栈里面,还是出栈(不可以作为路径的一个节点)。

这里再次分析问题,迷宫从左上角出发,如果能走到右下角,说明走通了(有路径)。否则在任何一节点卡死,则说明迷宫不通。这里使用的深度遍历,就是说 当前方向能走,则走下去。暂时不用管其他的方向,先沿着这个方向走下去(能走则一直走)。但是有一点需要注意:从当前节点走到下一个节点,那么这两个节点的方向应该改变为不能走。结束之后才真正把下个节点给入栈。

注:从左上角出发,为了尽可能快地到达右下角。迷宫的4个数组方向,规定方向的先后顺序为
1 → 2↓ 3← 4 ↑ 。

当行走过程中,发现走入了死路。(刚走过的路先被堵死,然后其他3个方向的路也走不了。那当然就是说这个节点 其实走不了,那于是就从栈里面删除这个节点,然后立即标注 刚才我走的这个路 是个死路,千万不可以再进去了。然后开始继续看原栈顶元素的其它方向了)。

最终源代码如下:

#include 
#include 
#include 
#include 
using namespace std;

//迷宫里面节点的那个方向数组的下标
const int RIGHT = 0;//右
const int DOWN = 1;//下
const int LEFT = 2;//左
const int UP = 3;//上
const int DirectNum = 4;//一个节点有4个方向

//迷宫节点
struct Node
{
	int x;//节点坐标
	int y;
	int node_val;//节点值

	int DirectArr[DirectNum];//节点方向数组 4个
};
/*
对于上面的那个方向数组 里面每个值:
YES 表示可以走
NO  表示不可以走

0 0 0 1 1
1 0 0 0 1
1 1 0 1 1
1 1 0 0 1
1 1 1 0 0
*/
const int YES = 5;//可以走
const int NO = 6;//不可以走
//迷宫类型
class Maze
{
public:
	Maze(int Row, int Col)//外部初始化迷宫
	{
		if (Row < 1 || Col < 1)
			throw"Init failed!!!";
		_Row = Row;
		_Col = Col;

		_pMazeArray = new Node*[Row];
		for (int i = 0; i < Row; ++i)
			_pMazeArray[i] = new Node[Col];
	}
	// 可以初始化迷宫节点的基本信息
	void initNode(int i,int j,int val)
	{
		_pMazeArray[i][j].x = i;
		_pMazeArray[i][j].y = j;
		_pMazeArray[i][j].node_val = val;
		//初始化 这个节点的四个方向都是不能走
		for (int k = 0; k < DirectNum; ++k)
			_pMazeArray[i][j].DirectArr[k] = NO;
		/*
		那么以后再做的时候,只需要去调整值为0的节点的方向就行了
		而且最妙的一点就在于:已经把这个边界的外围都封死了。
		下面再调整节点方向状态的时候,不需要再去调整了
		*/
	}

	void setNodeState()// 开始设置所有节点的四个方向的状态
	{
		for (int i = 0; i < _Row; ++i)
		{
			for (int j = 0; j < _Col; ++j)
			{
				if (_pMazeArray[i][j].node_val == 1)
				{
					continue;
					//对于节点值为1的节点 没必要修改其原方向数组
				}
				//节点值为0的节点 可以走的节点
				
				if (j < _Col - 1 && _pMazeArray[i][j + 1].node_val 
				== 0)//右边能走
				{
					_pMazeArray[i][j].DirectArr[RIGHT] = YES;
				}

				if ( i < _Row - 1 && _pMazeArray[i+1][j].node_val 
				== 0)//下边能走
				{
					_pMazeArray[i][j].DirectArr[DOWN] = YES;
				}
				//左边能走
				if ( j >0 && _pMazeArray[i][j-1].node_val == 0)
				{
					_pMazeArray[i][j].DirectArr[LEFT] = YES;
				}
				//上边能走
				if ( i >0 && _pMazeArray[i-1][j].node_val == 0)
				{
					_pMazeArray[i][j].DirectArr[UP] = YES;
				}
			}
			
		}
	}

	void searchMazePath()// 开始从左上角搜索迷宫的路径信息了
	{

		if (_pMazeArray[0][0].node_val == 1)
			return;//完蛋啊  入口堵死 进不去
		_resultArrayStack.push(_pMazeArray[0][0]);//把【0】【0】入栈
	
		while (!_resultArrayStack.empty())
		{
			Node CurTop = _resultArrayStack.top();

			int x = CurTop.x;
			int y = CurTop.y;

			//先看看走到终点木有?
			if (x == _Row - 1 && y == _Col - 1)
			{
				return;//找到了 不走了
			}
			/*$$$$$$$$$$$$$$$$$$$没有到,继续找$$$$$$$$$$$$$$$$$$*/
			if (_pMazeArray[x][y].DirectArr[RIGHT] == YES)//先往右嘛
			{
				//走了就把路封起来
				_pMazeArray[x][y].DirectArr[RIGHT] = NO;
				_pMazeArray[x][y + 1].DirectArr[LEFT] = NO;
				//把右边的入栈
				_resultArrayStack.push(_pMazeArray[x][y + 1]);
				continue;
			}
			//右边不行,往下
			if (_pMazeArray[x][y].DirectArr[DOWN] == YES)
			{
				//走了就把路封起来
				_pMazeArray[x][y].DirectArr[DOWN] = NO;	
				_pMazeArray[x + 1][y].DirectArr[UP] = NO;					
				//把右边的入栈
				_resultArrayStack.push(_pMazeArray[x + 1][y]);
				continue;
			}
			//下边不行,往左
			if (_pMazeArray[x][y].DirectArr[LEFT] == YES)
			{
				//走了就把路封起来
				_pMazeArray[x][y].DirectArr[LEFT] = NO;
				_pMazeArray[x][y - 1].DirectArr[RIGHT] = NO;
				_resultArrayStack.push(_pMazeArray[x][y - 1]);
				//把右边的入栈
				continue;
			}
			//左边不行,往上
			if (_pMazeArray[x][y].DirectArr[UP] == YES)
			{
			//走了就把路封起来
				_pMazeArray[x][y].DirectArr[UP] = NO;
				_pMazeArray[x - 1][y].DirectArr[DOWN] = NO;
				_resultArrayStack.push(_pMazeArray[x - 1][y]);
				//把上边的入栈
				continue;
			}
			
	/*$$$$$$$$$$$$$$$$$$$还要去走,说明到了死路了$$$$$$$$$$$$$$$$$$*/
			_resultArrayStack.pop();//这个节点走不了
		}

	}
	//上面的while 有两种可能:
	//1 走不通,然后把栈所有节点退完了
	//2 找到了,栈内是路径信息(反方向的)
	void showMazePath()// 打印迷宫路径搜索的结果
	{
		if (_resultArrayStack.empty())
		{
			cout << "找遍了 没有这个路径" << endl;
			return;
		}
		while (!_resultArrayStack.empty())//把节点的值 修改为*
		{
			Node CurTop = _resultArrayStack.top();

			int x = CurTop.x;
			int y = CurTop.y;
			_pMazeArray[x][y].node_val = '*';
			_resultArrayStack.pop();
		}

		for (int i = 0; i < _Row; ++i)
		{
			for (int j = 0; j < _Col; ++j)
			{
				if (_pMazeArray[i][j].node_val == '*')
					cout << "*" << " ";
				else cout << _pMazeArray[i][j].node_val << " ";
			}
			cout << endl;
		}
	}
private:
	int _Row;
	int _Col;

	Node** _pMazeArray;
	
	stack<Node>_resultArrayStack;//遍历中的栈
};



int main()
{
	cout << "请输入迷宫的行列数(例如:10 10):";
	int row, col, data;
	cin >> row >> col;

	Maze maze(row, col); // 创建迷宫对象
cout << "请输入迷宫的路径信息(0表示可以走,1表示不能走):" << endl;
	for (int i = 0; i < row; ++i)
	{
		for (int j = 0; j < col; ++j)
		{
			cin >> data;
			// 可以初始化迷宫节点的基本信息
			maze.initNode(i, j, data);
		}
	}

	// 开始设置所有节点的四个方向的状态
	maze.setNodeState();

	// 开始从左上角搜索迷宫的路径信息了
	maze.searchMazePath();
cout << "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" << endl;
	// 打印迷宫路径搜索的结果
	maze.showMazePath();

	return 0;
}

C++的学习心得和知识总结 第十四章(完)_第2张图片
此次写代码,花费了大量时间在
C++的学习心得和知识总结 第十四章(完)_第3张图片
先开始 这个地方写的是Node CurTop = _resultArrayStack.top(); 里面的CurTop,后来又修改成_resultArrayStack.top() 还是运行不出来结果。调试了5个多小时,心塞啊
多简单的问题 唉 !!!!!!!!!!!!

第二题:求迷宫的最短路径(广度优先遍历)

上面的深度优先,是走下去则沿着一个方向(规定好的 右 下 左 上),直至走出迷宫或者死胡同。当站在这个节点上,如果刚才规定好的那个方向可以走 则就按照那个方向前进。并不会去判断一下哪条路会近一些?例如 :上面的代码走下面的迷宫:
C++的学习心得和知识总结 第十四章(完)_第4张图片
这样走,明显不是最优解,绕了很大的胡同。
当然只是简单的更换行走方向顺序,肯定也是不合适的。

这里就要使用到广度优先遍历 来找到迷宫路径的最短路径。
提高情况下:深度优先遍历采用的是递归 当然我们这里使用了栈实现了非递归版本。而广度优先遍历采用的是 类似剥洋葱的方式,逐渐接近最终解。以达到取得最优解的结果。当然这里就非常自然地使用上了队列的结构。
C++的学习心得和知识总结 第十四章(完)_第5张图片
广度优先遍历的方式分析如下:从入口位置进入,把门口节点入队列。然后类似于 深度优先遍历,进入循环控制结构:队列不为空,然后检查队头元素的 4个方向的是否可行。依次把可以走的下一个位置给入队,注意:这里是所有可以走的下一步节点是都可以入队的。(当然是跟在队尾了) 处理完这个队头节点之后(4个方向都处理了),然后这个队头节点既可以走了。(当然在处理下一个这个可以走的位置的时候,节点间的移动状态也需要去置为NO)继续处理下一个队头元素。

经过循环之后,重点肯定是可以找到最短路径的。也就是我们在到达终点的时候,随着之前的入队、出队,可这些元素无论是有效的还是无效的,我们都没有保留下来。根本不确定 到底哪个元素才是属于这个最短路径上的节点呢?(深度优先遍历的栈最终保存的就是其路径信息)。所以这里就需要额外的花上一部分空间,来保存最短路径上的节点(行走过程中的这个最短路径节点之间的位置信息)都是哪些节点?
如下:最坏的情况无非就是 每个位置都可以走。
在这里插入图片描述
简单来做就是:把上面的二维数组(迷宫地图)的节点 一一映射到这个一位数组之上。(其实在内存之上,没有所谓的二维。只是抽象意义上的二维)

映射方法:二维数组的坐标(x,y)——》一位数组的坐标(x*Row+y)。这也即:一维数组的下标就标识着 二维数组上的某一个节点(唯一的)。

于是这样就好做了:从这个节点A到达下一个节点B,应该在一位数组B下标的数据域里面记载上A节点的二维坐标。(下次从B找它是从哪来的? 或者说 B的前一个节点元素是谁? 就可以很方便地根据B下标里面的二维坐标找到A)。

这样从最终的终点下标 在这个辅助数组里面往前找,直到找到门口节点。就可以把这个路径的节点信息 一一还原出来(一条完整的路径就出来了),这个过程是从后向前逆推的。
C++的学习心得和知识总结 第十四章(完)_第6张图片
源代码如下:

#include 
#include 
#include 
#include 
#include 
using namespace std;

//迷宫里面节点的那个方向数组的下标
const int RIGHT = 0;//右
const int DOWN = 1;//下
const int LEFT = 2;//左
const int UP = 3;//上
const int DirectNum = 4;//一个节点有4个方向

//迷宫节点
struct Node
{
	int x;//节点坐标
	int y;
	int node_val;//节点值

	int DirectArr[DirectNum];//节点方向数组 4个
};
/*
对于上面的那个方向数组 里面每个值:
YES 表示可以走
NO  表示不可以走

0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 0 0 0 0 1
1 0 1 1 1 1
1 0 0 0 0 0
*/
const int YES = 5;//可以走
const int NO = 6;//不可以走
//迷宫类型
class Maze
{
public:
	Maze(int Row, int Col)//外部初始化迷宫
	{
		if (Row < 1 || Col < 1)
			throw"Init failed!!!";
		_Row = Row;
		_Col = Col;

		_pMazeArray = new Node*[Row];
		for (int i = 0; i < Row; ++i)
			_pMazeArray[i] = new Node[Col];

		//给辅助数组开辟空间  按照最坏情况处理
		_result_node_information.resize(Row * Col);
	}
	void initNode(int i,int j,int val)// 可以初始化迷宫节点的基本信息
	{
		_pMazeArray[i][j].x = i;
		_pMazeArray[i][j].y = j;
		_pMazeArray[i][j].node_val = val;
		//初始化 这个节点的四个方向都是不能走
		for (int k = 0; k < DirectNum; ++k)
			_pMazeArray[i][j].DirectArr[k] = NO;
		/*
		那么以后再做的时候,只需要去调整值为0的节点的方向就行了
		而且最妙的一点就在于:已经把这个边界的外围都封死了。
		下面再调整节点方向状态的时候,不需要再去调整了
		*/
	}

	void setNodeState()// 开始设置所有节点的四个方向的状态
	{
		for (int i = 0; i < _Row; ++i)
		{
			for (int j = 0; j < _Col; ++j)
			{
				if (_pMazeArray[i][j].node_val == 1)
				{
					continue;
					//对于节点值为1的节点 没必要修改其原方向数组
				}
				//节点值为0的节点 可以走的节点
				
				if (j < _Col - 1 && _pMazeArray[i][j + 1].node_val 
				== 0)//右边能走
				{
					_pMazeArray[i][j].DirectArr[RIGHT] = YES;
				}

				if ( i < _Row - 1 && _pMazeArray[i+1][j].node_val 
				== 0)//下边能走
				{
					_pMazeArray[i][j].DirectArr[DOWN] = YES;
				}
				//左边能走
				if ( j >0 && _pMazeArray[i][j-1].node_val == 0)
				{
					_pMazeArray[i][j].DirectArr[LEFT] = YES;
				}
				//上边能走
				if ( i >0 && _pMazeArray[i-1][j].node_val == 0)
				{
					_pMazeArray[i][j].DirectArr[UP] = YES;
				}
			}
			
		}
	}

	void searchMazePath()// 开始从左上角搜索迷宫的路径信息了
	{

		if (_pMazeArray[0][0].node_val == 1)
			return;//完蛋啊  入口堵死 进不去
		_use_Queue_BFS.push(_pMazeArray[0][0]);//把【0】【0】入队列
	
		while (!_use_Queue_BFS.empty())
		{
			//得到队头元素信息
			Node CurTop = _use_Queue_BFS.front();
			int x = CurTop.x;
			int y = CurTop.y;

			//下面是处理队头元素的四个方向的 看是否可以走
			if (_pMazeArray[x][y].DirectArr[RIGHT] == YES)//先往右嘛
			{
				_pMazeArray[x][y].DirectArr[RIGHT] = NO;//走了就把路封起来
				_pMazeArray[x][y + 1].DirectArr[LEFT] = NO;

		//下面这句话是 记录当前节点的二维坐标到下一个节点(右节点)的一维数组中去
				_result_node_information[x * _Row + y + 1] 
				= _pMazeArray[x][y];
				//把右边的入队列
				_use_Queue_BFS.push(_pMazeArray[x][y + 1]);

				//先看看走到终点木有?
				if (check_is_end(_pMazeArray[x][y+1]))
				{
					return;//找到了 不走了 也不需要把终点位置去记录给别人
				}
			}

			if (_pMazeArray[x][y].DirectArr[DOWN] == YES)//右边不行,往下
			{
				_pMazeArray[x][y].DirectArr[DOWN] = NO;//走了就把路封起来
				_pMazeArray[x + 1][y].DirectArr[UP] = NO;

		//下面这句话是 记录当前节点的二维坐标到下一个节点(下节点)的一维数组中去
				_result_node_information[(x + 1) * _Row + y] 
				= _pMazeArray[x][y];
				//把下边的入队列
				_use_Queue_BFS.push(_pMazeArray[x + 1][y]);
				//先看看走到终点木有?
				if (check_is_end(_pMazeArray[x+1][y]))
				{
					return;//找到了 不走了 也不需要把终点位置去记录给别人
				}
			}
			//下边不行,往左
			if (_pMazeArray[x][y].DirectArr[LEFT] == YES)
			{
			//走了就把路封起来
				_pMazeArray[x][y].DirectArr[LEFT] = NO;
				_pMazeArray[x][y - 1].DirectArr[RIGHT] = NO;

		//下面这句话是 记录当前节点的二维坐标到下一个节点(左节点)的一维数组中去
				_result_node_information[x * _Row + y - 1] 
				= _pMazeArray[x][y];
				//把左边的入队列
				_use_Queue_BFS.push(_pMazeArray[x][y - 1]);
				//先看看走到终点木有?
				if (check_is_end(_pMazeArray[x][y-1]))
				{
					return;//找到了 不走了 也不需要把终点位置去记录给别人
				}
			}

			if (_pMazeArray[x][y].DirectArr[UP] == YES)//左边不行,往上
			{
				_pMazeArray[x][y].DirectArr[UP] = NO;//走了就把路封起来
				_pMazeArray[x - 1][y].DirectArr[DOWN] = NO;

		//下面这句话是 记录当前节点的二维坐标到下一个节点(上节点)的一维数组中去
				_result_node_information[(x - 1) * _Row + y]
				= _pMazeArray[x][y];
				//把上边的入队列
				_use_Queue_BFS.push(_pMazeArray[x - 1][y]);
				//先看看走到终点木有?
				if (check_is_end(_pMazeArray[x-1][y]))
				{
					return;//找到了 不走了 也不需要把终点位置去记录给别人
				}
			}
			/*$$$$$$$$队头的四个方向的下一步都处理结束了$$$$$$$*/
			_use_Queue_BFS.pop();//这个队头节点 处理完了
		}

	}
	//上面的while 有两种可能:
	//1 走不通,然后把队列所有节点退完了
	//2 找到了,路径信息都保存在一维辅助数组里面了
	void showMazePath()// 打印迷宫路径搜索的结果
	{
		if (_use_Queue_BFS.empty())
		{
			cout << "找遍了 没有这个路径" << endl;
			return;
		}
		//由终点节点出发 向前回溯(在一位数组里面)
		int x = _Row - 1;//这个终点是肯定在一维数组里面的
		int y = _Col - 1;
		while (1)
		{
			_pMazeArray[x][y].node_val = '*';//修改路径上的值
			if (x == 0 && y == 0)
				break;
			//把这个节点的前一个节点位置 取出来 继续向前回溯
			Node node = _result_node_information[x * _Row + y];
			x = node.x;
			y = node.y;
		}

		for (int i = 0; i < _Row; ++i)
		{
			for (int j = 0; j < _Col; ++j)
			{
				if (_pMazeArray[i][j].node_val == '*')
					cout << "*" << " ";
				else cout << _pMazeArray[i][j].node_val << " ";
			}
			cout << endl;
		}
	}
private:
	int _Row;
	int _Col;

	Node** _pMazeArray;
	//判断一下 当前节点所走的下一个节点是否是终点
	bool check_is_end(Node& node)
	{
		return node.x == _Row - 1 && node.y == _Col - 1;
	}
	queue<Node>_use_Queue_BFS;//遍历中所使用的队列

	//保存BFS的最短路径信息  辅助数组
	vector<Node>_result_node_information;
};



int main()
{
	cout << "请输入迷宫的行列数(例如:10 10):";
	int row, col, data;
	cin >> row >> col;

	Maze maze(row, col); // 创建迷宫对象

	cout << "请输入迷宫的路径信息(0表示可以走,1表示不能走):" << endl;
	for (int i = 0; i < row; ++i)
	{
		for (int j = 0; j < col; ++j)
		{
			cin >> data;
			// 可以初始化迷宫节点的基本信息
			maze.initNode(i, j, data);
		}
	}
	// 开始设置所有节点的四个方向的状态
	maze.setNodeState();

	// 开始从左上角搜索迷宫的路径信息了
	maze.searchMazePath();
	cout << "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" << endl;
	// 打印迷宫路径搜索的结果
	maze.showMazePath();

	return 0;
}

C++的学习心得和知识总结 第十四章(完)_第7张图片

第三题:大数的加减法

题目要求:实现大数的加减法。而且在主函数里面有 对象的输出,所以需要提供 operator+ operator- operator<<这三个方法。如果有时间,可以再实现一下 乘法和除法的实现。

#include 
#include 
#include 

using namespace std;

class BigInt
{//要求实现BigInt的成员方法,对大数据进行加减法 和 输出
public:
	BigInt(string str):_strDigit(str){}
	
private:
	string _strDigit;//使用字符串来存储内置类型存不下的大数据

	//需要提供以下方法:
	friend ostream& operator<<(ostream& out, const BigInt& src);
	friend BigInt operator+(const BigInt& src1, const BigInt& src2);
	friend BigInt operator-(const BigInt& src1, const BigInt& src2);
};
int main()
{
	BigInt int1("9785645649886874535428765");
	BigInt int2("28937697857832167849697653231243");
	BigInt int3("9785645649886874535428765");
	//28937707643477817736572188660008
	//28937707643477817736572188660008
	cout << int1 + int2 << endl;
	//28937688072186517962823117802478
	//28937688072186517962823117802478
	cout << int1 - int2 << endl;

	BigInt int4("123");
	BigInt int5("99");
	cout << int5 - int4 << endl;

	return 0;
}

这里给定的BigInt对象,对外来说看着是BigInt类型的对象,但是对内,主要是看其成员属性其底层也只是一个string对象(成员变量)。这最终遍历的也就是这个字符串对象。

#include 
#include 
#include //泛型算法

using namespace std;

class BigInt
{//要求实现BigInt的成员方法,对大数据进行加减法 和 输出
public:
	BigInt(string str):_strDigit(str){}
	//以上构造函数,通过传入一个string对象,来构造大数对象
private:
	string _strDigit;//使用字符串来存储内置类型存不下的大数据

	//需要提供以下方法:
	friend ostream& operator<<(ostream& out, const BigInt& src);
	friend BigInt operator+(const BigInt& src1, const BigInt& src2);
	friend BigInt operator-(const BigInt& src1, const BigInt& src2);
};
ostream& operator<<(ostream& out, const BigInt& src)
{
	out << src._strDigit;
	return out;
}
BigInt operator+(const BigInt& src1, const BigInt& src2)
{
	/*
	个位数开始相加,从后向前遍历 src1 src2
	可以把结果存在一个 string对象里面,然后构造函数构造对象返回即可
	这两个大数同时从个位开始相加,有可能同时结束,有可能有一个先结束
	每一位相加的时候,都要考虑到进位问题(通过一个标识位)
	*/
	string result;
	bool tag = false;//默认没有进位
	int len1 = src1._strDigit.length();
	int len2 = src2._strDigit.size();

	int i = len1 - 1, j = len2 - 1;
	for (; i >= 0 && j >= 0; --i, --j)
	{
		int a = 0;
		if (tag)
		{
			a += 1;
			tag = false;
		}
			
		a += src1._strDigit[i] - '0' + src2._strDigit[j] - '0';
		if (a >= 10)
		{
			a = a % 10;
			tag = true;
		}
		result.push_back(a + '0');
	}
	if (i >= 0)//有一个数还没有完
	{
		for (; i >= 0; --i)
		{
			int a = 0;
			if (tag)//有可能上面还有进位
			{
				a += 1;
				tag = false;
			}
			a += src1._strDigit[i] - '0';
			
			if (a >= 10)
			{
				a = a % 10;
				tag = true;
			}
			result.push_back(a + '0');
		}
	}
	else if (j >= 0)
	{
		for (; j >= 0; --j)
		{
			int a = 0;
			if (tag)//有可能上面还有进位
			{
				a += 1;
				tag = false;
			}

			a += src1._strDigit[j] - '0';

			if (a >= 10)
			{
				a = a % 10;
				tag = true;
			}
			result.push_back(a + '0');
		}
	}
	if(tag)//最后一位有可能上面遗留下还有进位
		result.push_back(1 + '0');
	reverse(result.begin(), result.end());
	return BigInt(result);
}
BigInt operator-(const BigInt& src1, const BigInt& src2)
{
	/*
	个位数开始相减,从后向前遍历 src1 src2
	可以把结果存在一个 string对象里面,然后构造函数构造对象返回即可
	这两个大数同时从个位开始相加减,有可能同时结束,有可能有一个先结束
	每一位相减的时候,都要考虑到借位问题(通过一个标识位)
	*/
	string result;
	bool tag = false;//默认没有借位
	int len1 = src1._strDigit.length();
	int len2 = src2._strDigit.size();

	bool data_symbol = true;//表示正数
	string maxStr_src = src1._strDigit;//默认
	string minStr_src = src2._strDigit;
	if (len1 > len2 || (len1 == len2) && src1._strDigit > src2._strDigit)
	{
		//保持不变
	}
	else if (len1 < len2||(len1==len2)&&src1._strDigit<src2._strDigit)
	{
		data_symbol = false;//表示负数
		maxStr_src = src2._strDigit;//需要更改
		minStr_src = src1._strDigit;
	}
	else//这两个大数 是一样的
	{
		result.push_back('0');
		return BigInt(result);
	}

	int i = maxStr_src.size() - 1, j = minStr_src.size() - 1;
	for (; i >= 0 && j >= 0; --i, --j)
	{
		int a = 0;
		if (tag)//先看看有没有借位
		{
			a -= 1;//这里把借位还回去
			tag = false;
		}

		a += (maxStr_src[i] - '0') - (minStr_src[j] - '0');
		if (a <0)
		{
			a = a + 10;//这里发生了借位
			tag = true;
		}
		result.push_back(a + '0');
	}
	if (i >= 0)//有一个数还没有完
	{
		for (; i >= 0; --i)
		{
			int a = 0;
			if (tag)
			{
				a -= 1;
				tag = false;
			}
			a += maxStr_src[i] - '0';

			if (a >= 10)
			{
				a = a + 10;
				tag = true;
			}
			result.push_back(a + '0');
		}
	}
	auto it = result.rbegin();//删除前面的多余数字0  版本二
	for (; it != result.rend(); ++it)
	{
		if (*it != '0')
		{
			break;
		}
	}
	string str;
	for (; it != result.rend(); ++it)
	{
		str.push_back(*it);
	}
	
/*
	reverse(result.begin(), result.end());
	for (auto it = result.begin(); it != result.end(); ++it)
	{//删除前面的多余数字0  版本一
		if (*it == '0')
		{
			it = result.erase(it);//删除前面的多余数字0
		}
		else break;
	}
*/	
	if (!data_symbol)//这个结果是个负数
	{
		string::iterator it = str.begin();
		str.insert(it, '-');
	}
	

	return BigInt(str);
}
int main()
{
	BigInt int1("9785645649886874535428765");
	BigInt int2("28937697857832167849697653231243");
	BigInt int3("9785645649886874535428765");
	//28937707643477817736572188660008
	//28937707643477817736572188660008
	//
	cout << int2 + int3 << endl;

	BigInt int4("888");
	BigInt int5("999");
	cout << int5 - int4 << endl;
	cout << int4 - int4 << endl;
	//28937688072186517962823117802478
	//28937688072186517962823117802478
	cout << int2 - int3 << endl;

	BigInt int6("123");
	BigInt int7("99");
	cout << int7 - int6 << endl;

	return 0;
}

C++的学习心得和知识总结 第十四章(完)_第8张图片

第四题:海量数据Top K系列问题和查重问题

大量数据求 Top K、查重
(1)值最大的前k个 或者 值最小的前k个。
(2)值第k大 或者 第k小的那和数据是多少
(3)哪些数据是重复的?重复了多少次?
(4)去重复问题

本节重点:海量数据的查重问题
C++的学习心得和知识总结 第十四章(完)_第9张图片
但凡是查重复问题:就是要去使用增删查 近似达到O(1)时间复杂度的哈希表。如果是对于内存没有太大限制的场景,就可以一次使用哈希表。(牺牲空间换时间)。
(在32位Linux系统之下,一个int占4字节。10亿个数据,占内存大概为1*4G)所以说只是把这些数据存起来的话,就需要大量的内存(若是使用链式哈希表,除了数据域还有指针域),当然是不现实的。
(这里我们讨论的都是整数类型的查重)
如果是不允许这么大的内存使用,则这个时候需要使用上 分而治之的思想,把大量的数据量进行一个划分。
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
(这里将使用一个Bloom Filter布隆过滤器进行大量数据的查重)
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
倘若数据类型是 字符串类型,则也是可以使用哈希表和布隆过滤器的。可是对于字符串类型,我们有更好的处理办法:TrieTree字典树(前缀树)
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
(这里将使用一个进行TrieTree字典树(前缀树)大量数据的查重)
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
基于哈希表实现的undered_map undered_set。是最好用的工具 其中链式哈希表,是:解决哈希冲突是用链表把产生哈希冲突的数据连接起来,因此每一个节点除了保存整数数据,还需要保存指针域,因此10亿个整数,每个整数在多一个指针大小空间,那么整个链式哈希表算下来,大约需要4G(数据总数)+4G(指针总数)= 8G的内存空间。
C++的学习心得和知识总结 第十四章(完)_第10张图片
问题1 :下面的数组,哪些数字重复了,以及重复了多少次?(没有提及内存的情况,意思就是重在了解解法思想)

#include 
#include 
#include 
using namespace std;
int main()
{
	srand((unsigned)time(nullptr));
	const int DataSize = 40;//数据量  对内存没有限制
	int dataarr[DataSize] = { 0 };
	for (int i = 0; i < DataSize; ++i)
	{
		dataarr[i] = rand() % 40 + 1;
	}

	/*
	问题1 :上面的数组,哪些数字重复了,以及重复了多少次
	*/
	unordered_map<int, int>mymap;//键是数据本身,值为出现的次数
	for (int val : dataarr)
	{
		mymap[val]++;
		/*
		【】如果这个键存在则返回对应的值,然后进行++
		没有存在  则在mymap里面插入一个val为键,0为值(int类型默认产生)
		的pair对象。然后++表示出现了一次
		*/
	}
	//下面是数据的统计结果的输出
	for (unordered_map<int, int>::iterator it = mymap.begin();
	 it != mymap.end(); ++it)
	{
		if (it->second > 1)
cout << it->first << "出现了:" << it->second << "次" << endl;
	}
	cout << "**************************" << endl;
	for (pair<int,int>mypair : mymap)
	{
		if (mypair.second > 1)
cout << mypair.first << "出现了:" << mypair.second << "次" << endl;
	}
	return 0;
}

C++的学习心得和知识总结 第十四章(完)_第11张图片
mymap对象是定义在栈上的,所以数据量太大的情况就会导致。栈空间不够使用,导致程序崩溃

问题2 :给定一个文件(里面有50亿数据),哪些数字重复了,以及重复了多少次
内存限制在400M

50亿个数据(整数)大概在20G的大小。倘若再使用上哈希表(40G:这是最差的结果了),所以不可能一下子把全部的数据都加载到这个Map表里面。
C++的学习心得和知识总结 第十四章(完)_第12张图片
从大文件里面读出每一个data,然后mod文件个数(哈希映射函数:除留余数法),把这个数据放到对应序号的小文件中。为了减少哈希冲突,最好是个素数。这样做的话,值相同又经过同一个哈希映射函数,当然最后放在了同一个小文件中。(当然为了减少密集,把小文件个数上提(这样一个小文件的大小就不会超过400M了)。一个进程允许文件的个数大概到1024个)

然后对于每个小文件就可以读入到内存当中了,然后进行类似于上面的操作了。
这就是分而治之的思想
例子如下:这里50G/400M =128个 。做法就是将大文件的数据都通过除留余数法,把数据分散到众多的小文件里面,然后这些小文件每次都是可以直接加载到有所限制的内存之中的,然后进行操作。最终合并这些小文件的结果,得到最终的结果。
代码如下:

#include 
#include 
#include 
#include 
using namespace std;
/*
	问题2 :给定一个文件(里面有50亿数据),哪些数字重复了,以及重复了多少次
	内存限制在400M
*/
#define  _CRT_SECURE_NO_WARNINGS
int main()
{
	//问题描述如下:求下面这个大数据文件中的哪些数字重复,以及重复次数
	srand((unsigned)time(nullptr));
	const int DataSize = 15;//数据量  对内存有限制
	unsigned int* filebuffer = new unsigned int[DataSize];//开辟缓冲区
	FILE* pfile = fopen("test_data.txt", "wb");
	if (pfile == nullptr)
	{
		return -1;
	}
	for (int i = 0; i < DataSize; ++i)
	{
		filebuffer[i] = rand() % 10 + 1;	
	}
	fwrite(filebuffer, sizeof(int), DataSize, pfile);
	fclose(pfile);
	delete[]filebuffer;

	////////////////////////////////////////////////////////
	//打开源数据文本大文件
	FILE* Bigfile_ptr = fopen("test_data.txt", "rb");

	if (nullptr == Bigfile_ptr)
		throw"this file is empty!!!";
	//下面是进行大文件的数据映射切割
	const int small_file_nuber = 3;//小文件个数
	FILE* small_file_arr[small_file_nuber] = { nullptr };
	for (int i = 0; i < small_file_nuber; ++i)
	{
		char small_file_name[20];//存放小文件的文件名
		//给小文件起名字
		sprintf(small_file_name, "test_data%d.txt", i + 1);
		//给文件数组每一个文件初始化指针
		small_file_arr[i] = fopen(small_file_name, "wb+");
	}
	//下面是进行 哈希映射 数据分流
	int data;
	while (fread(&data, 4, 1, Bigfile_ptr) > 0)//每次读一个
	{
		int file_index = data % small_file_nuber;//哈希映射
		//写入数据
		fwrite(&data, 4, 1, small_file_arr[file_index]);
	}
	

	unordered_map<int, int>mymap;//键是数据本身,值为出现的次数
	for (int i = 0; i < small_file_nuber; ++i)//处理这3个小文件
	{
		// 恢复小文件的文件指针到起始位置
		fseek(small_file_arr[i], 0, SEEK_SET);

		while(fread(&data,4,1,small_file_arr[i]))
		{
			mymap[data]++;
			/*
			【】如果这个键存在则返回对应的值,然后进行++
			没有存在  则在mymap里面插入一个val为键,0为值(int类型默认产生)
			的pair对象。然后++表示出现了一次
			*/
		}
		//统计结束了
		for (pair<int, int>mypair : mymap)
		{
			if (mypair.second > 0)
				cout << mypair.first << "出现了:" 
				<< mypair.second << "次" << endl;
		}
		cout << "***************************" << endl;
		// 清空哈希表,进行下一个小文件的数据统计
		mymap.clear();
	}
	for (int i = 0; i < small_file_nuber; ++i)//关闭这3个小文件
	{
		fclose(small_file_arr[i]);
	}
	
	return 0;
}

C++的学习心得和知识总结 第十四章(完)_第13张图片
C++的学习心得和知识总结 第十四章(完)_第14张图片
问题3:
C++的学习心得和知识总结 第十四章(完)_第15张图片
10亿个数据,相当于1G的整数。也就是整数就4G了,使用上链式哈希表,就得到8G的结果。需要8G的内存才可以读进去,可是现在内存限制在400M上。约莫分上37个小文件就可以完美解决问题了。

这里的解决方案是:把a和b这两个大文件,划分成个数相等的一些小文件。这里照样使用的是分而治之的策略,比如都划分为37个小文件。
C++的学习心得和知识总结 第十四章(完)_第16张图片
分别从a和b文件里面读取数据data。然后data%37=file_index.得到数据的下标,然后把数据存放在对于号的小文件之中。
在这里插入图片描述
毕竟这里采用的是,相同的哈希映射函数。最后处理的时候,是对应下标的小文件两两一起处理。

#include 
#include 
#include 
#include 
using namespace std;
/*
	问题3 :给定2个文件(里面都有10亿数据),哪些数字重复了,以及重复了多少次
	内存限制在400M

	这里我采用一个vector将最后的结果数据都保存一下
*/
#define  _CRT_SECURE_NO_WARNINGS
int main()
{
	//问题描述如下:求下面这2个大数据文件中的哪些数字重复,以及重复次数
	srand((unsigned)time(nullptr));
	const int DataSize = 15;//数据量  对内存有限制
	unsigned int* filebuffer = new unsigned int[DataSize];//开辟缓冲区
	FILE* pfile1 = fopen("test_data.txt_A", "wb");//这是给A文件传入数据
	if (pfile1 == nullptr)
	{
		return -1;
	}
	for (int i = 0; i < DataSize; ++i)
	{
		filebuffer[i] = rand() % 10 + 1;	
	}
	fwrite(filebuffer, sizeof(int), DataSize, pfile1);
	fclose(pfile1);

	FILE* pfile2 = fopen("test_data.txt_B", "wb");//这是给B文件传入数据
	if (pfile2 == nullptr)
	{
		return -1;
	}
	for (int i = 0; i < DataSize; ++i)
	{
		filebuffer[i] = rand() % 10 + 1;
	}
	fwrite(filebuffer, sizeof(int), DataSize, pfile2);
	fclose(pfile2);
	delete[]filebuffer;

	////////////////////////////////////////////////////////
	vector<pair<int, int>>result_vec;//存放结果的
	//打开源数据文本大文件A
	FILE* Bigfile_ptr1 = fopen("test_data.txt_A", "rb");
	//打开源数据文本大文件B
	FILE* Bigfile_ptr2 = fopen("test_data.txt_B", "rb");
	if (nullptr == Bigfile_ptr1 ||nullptr==Bigfile_ptr2)
		throw"this file is empty!!!";
	//下面是进行大文件的数据映射切割
	const int small_file_nuber = 3;//小文件个数
	//A 的小文件指针
	FILE* small_file_arr1[small_file_nuber] = { nullptr };
	//B 的小文件指针
	FILE* small_file_arr2[small_file_nuber] = { nullptr };
	for (int i = 0; i < small_file_nuber; ++i)
	{
		char small_file_name[20];//存放小文件的文件名
		//给A 的小文件起名字
		sprintf(small_file_name, "A_test_data%d.txt", i + 1);
		//给文件数组每一个文件初始化指针
		small_file_arr1[i] = fopen(small_file_name, "wb+");
		//给B 的小文件起名字
		sprintf(small_file_name, "B_test_data%d.txt", i + 1);
		//给文件数组每一个文件初始化指针
		small_file_arr2[i] = fopen(small_file_name, "wb+");
	}
	//下面是进行 哈希映射 数据分流
	int data;
	while (fread(&data, 4, 1, Bigfile_ptr1) > 0)//A 文件每次读一个
	{
		int file_index = data % small_file_nuber;//哈希映射
		fwrite(&data, 4, 1, small_file_arr1[file_index]);//写入数据
	}
	while (fread(&data, 4, 1, Bigfile_ptr2) > 0)//B 文件每次读一个
	{
		int file_index = data % small_file_nuber;//哈希映射
		fwrite(&data, 4, 1, small_file_arr2[file_index]);//写入数据
	}
	unordered_map<int, int>mymap;//键是数据本身,值为出现的次数
	for (int i = 0; i < small_file_nuber; ++i)//处理这3个小文件
	{
		// 恢复小文件的文件指针到起始位置
		fseek(small_file_arr1[i], 0, SEEK_SET);
		fseek(small_file_arr2[i], 0, SEEK_SET);
		//先统计A的index号小文件
		while(fread(&data,4,1,small_file_arr1[i]))
		{
			mymap[data]++;
			/*
			【】如果这个键存在则返回对应的值,然后进行++
			没有存在  则在mymap里面插入一个val为键,0为值(int类型默认产生)
			的pair对象。然后++表示出现了一次
			*/
		}
		//再统计B的index号小文件
		while (fread(&data, 4, 1, small_file_arr2[i]))
		{
			mymap[data]++;
		}
		//统计结束了
		for (pair<int,int>val : mymap)
		{
			result_vec.push_back(val);
		}
		
		// 清空哈希表,进行下一个小文件的数据统计
		mymap.clear();
	}
	for (int i = 0; i < small_file_nuber; ++i)//关闭这3个小文件
	{
		fclose(small_file_arr1[i]);
		fclose(small_file_arr2[i]);
	}	
	
	bool tag = true;
	for (int i=0;i<result_vec.size();++i)
	{
		pair<int, int>& val = result_vec[i];
		if (val.first % 3 == 0 && tag)
		{
			while (tag)
			{
				cout << "mod3=0的   如下:" << endl;
				tag = false;//只让上面那句打印一次
			}
		}
		if (val.first % 3 == 0)
		{
		cout << val.first << "出现了" << val.second << "次" << endl;
		continue;
		}
		

		if (val.first % 3 == 1 && !tag)
		{
			while (!tag)
			{
				cout << "******************************" << endl;
				cout << "mod3=1的   如下:" << endl;
				tag = true;//只让上面那句打印一次
			}
		}
		if (val.first % 3 == 1)
		{
		cout << val.first << "出现了" << val.second << "次" << endl;
		continue;
		}
		
		if (val.first % 3 == 2 && tag)
		{
			while (tag)
			{
				cout << "******************************" << endl;
				cout << "mod3=2的   如下:" << endl;
				tag = false;//只让上面那句打印一次
			}
		}
		if (val.first % 3 == 2)
		{
		cout << val.first << "出现了" << val.second << "次" << endl;
		continue;
		}
	}
	return 0;
}

C++的学习心得和知识总结 第十四章(完)_第17张图片
C++的学习心得和知识总结 第十四章(完)_第18张图片

第五题:海量数据Top K

C++的学习心得和知识总结 第十四章(完)_第19张图片
假如这里如果找的是前K大元素,需要的是小根堆。具体做法:把这么多数据的前k个元素放进去这个小根堆里面,(找前K大元素,就是在逐渐淘汰小值的过程),于是这前K个元素的最小值在堆顶存在。然后再逐渐遍历余下的整数,这些整数都和此时的堆顶进行比较。
(1)如果比堆顶元素小,则继续遍历下一个
(2)比堆顶大,则证明此时的堆顶元素,肯定不是前k大的元素。于是小根堆的堆顶出堆,然后这个比较大点的元素入堆。
(3)直至剩下数据遍历结束,此时的小根堆里面就保存的是前K大元素。
例如:
C++的学习心得和知识总结 第十四章(完)_第20张图片
如果是找前K小元素,需要的是大根堆。 具体做法:把这么多数据的前k个元素放进去这个大根堆里面,(找前K小元素,就是在逐渐淘汰大值的过程),于是这前K个元素的最大值在堆顶存在。然后再逐渐遍历余下的整数,这些整数都和此时的堆顶进行比较。
(1)如果比堆顶元素大,则继续遍历下一个
(2)比堆顶小,则证明此时的堆顶元素,肯定不是前k小的元素。于是小根堆的堆顶出堆,然后这个比较小点的元素入堆。
(3)直至剩下数据遍历结束,此时的大根堆里面就保存的是前K小元素。
如果是找第K小元素,需要的是大根堆的堆顶
如果是找第K大元素,需要的是小根堆的堆顶
写代码的时候,我们也不需要去自定义 大小根堆:
在这里插入图片描述
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
解法2:快排分割函数(效率会比大小根堆更高一些)
假如问题场景如下:共8个数据
在这里插入图片描述
对于快排分割函数而言,一趟下来:暂定把23作为基准数 ,可以使得小于23的调整到左边;大于23的调整到右边。基准数23 就可以算是调整结束了 O(log n)

实现步骤如下:
(1)把23先保存起来。从后向前,16小于23,则16调整到23位置处。如下:
在这里插入图片描述
(2)然后从左再向右找,遇到了45,大于23,则45调整到最后面位置上。
在这里插入图片描述
(3)再从右向左找:找到一个比23小的。5调整
在这里插入图片描述
(4)不存在从左往右,比23 小的值了。于是23可以归位了
在这里插入图片描述
于是、于是、于是一趟快排分割函数,23的位置也就调整好了。小于23的调整到左边;大于23的调整到右边。如下:
在这里插入图片描述
23 的数组下标是3(index),表示着23 是整个数组的第4(index+1)小的数据。而且前4小的数据就是23 前面的和23本身。

如果是找前k大的话,表示着23 是整个数组的第5大的数据。而且前4大的数据就是23 后面的和23本身。

如果这里找的是第2小的,则说明这个数据在23的左面。然后在23的左边进行 一次快排分割函数。如果这里找的是第6小的,则说明这个数据在23的右面。然后在23的右边进行 一次快排分割函数。

经过快排分割函数,能够在O(lgn)时间内,把小于基准数的整数调整到左边,把大于基准数的整数调整到右边,基准数(index)就可以认为是第(index+1)小的整数了,也即 [0,(index)]就是前index+1小的整数了

问题1:求vector容器中元素值最大的前10个数字。代码如下:

#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
	/*
	问题描述:求vector容器中元素值最大的前10个数字
	*/
	vector<int> vec;
	for (int i = 0; i < 100000; ++i)
	{
		vec.push_back(rand() + i);
	}

	// 算法的时间复杂度:O(n)
	//优先级队列 默认是一个大根堆,函数对象默认less,先更改为greater函数对象
	// 定义小根堆  priority_queue maxHeap;
	priority_queue<int, vector<int>, greater<int>> minHeap;
	// 先往小根堆放入10个元素
	int k = 0;
	for (; k < 10; ++k)
	{
		minHeap.push(vec[k]);
	}
	//构建一个小根堆,堆顶默认是最小的值
	/*
	遍历剩下的元素依次和堆顶元素进行比较,如果比堆顶元素大,
	那么删除堆顶元素,把当前元素添加到小根堆中,元素遍历完成,
	堆中剩下的10个元素,就是值最大的10个元素
	*/
	for (; k < vec.size(); ++k)
	{
		if (vec[k] > minHeap.top()) // O(log_2_10)
		{
			minHeap.pop();
			minHeap.push(vec[k]);
		}
	}

	// 打印结果   这个是找前K个,如果是找第K个,那么只打印堆顶元素就可以了
	while (!minHeap.empty())
	{
		cout << minHeap.top() << " ";
		minHeap.pop();
	}
	cout << endl;

	return 0;
}

C++的学习心得和知识总结 第十四章(完)_第21张图片
算法的时间复杂度O(n):遍历这个容器。

问题2:求vector容器中元素值第10个小的数字。算法的时间复杂度O(log n)。代码如下:

#include 
#include 
#include 
#include 
#include 
using namespace std;
/*
快排分割函数,选择arr[i]号元素作为基数,把小于arr[i]的元素
调整到左边,把大于arr[i]的元素调整到右边并返回基数位置的下标
*/

//待排序的数组的起始下标  和  末尾下标
int partation(vector<int>& arr, int i, int j)
{
	int k = arr[i];//此时的k就是基准数
	while (i < j)
	{
		while (i < j && arr[j] >= k)//从后往前找比基准数小的值
			j--;
		if (i < j)//把这个小一点的进行一个赋值移动
			arr[i++] = arr[j];

		while (i < j && arr[i] < k)//从前往后找比基准数大的值
			i++;
		if (i < j)//把这个大一点的进行一个赋值移动
			arr[j--] = arr[i];
	}
	arr[i] = k;//给基准数归为
	return i;//返回基准数的下标
}
/*
params:
1.vector &arr: 存储元素的容器
2.int i:数据范围的起始下标
3.int j:数据范围的末尾下标
4.int k:第k个元素
功能描述:通过快排分割函数递归求解第k小的数字,并返回它的值
*/
int selectNoK(vector<int>& arr, int i, int j, int k)
{
	int pos = partation(arr, i, j);//得到一次快排分割后的基准数下标
	if (pos == k - 1)
		return pos;//找到了,返回这个POS可以做更多事情
	else if (pos < k - 1)
		return selectNoK(arr, pos + 1, j, k);//右边的下一个位置接着找
	else
		return selectNoK(arr, i, pos - 1, k);//左边的上一个位置接着找
}
int main()
{
	/*
	求vector容器中元素第10小的元素值  前10小的
	*/
	vector<int> vec;
	for (int i = 0; i < 100000; ++i)
	{
		vec.push_back(rand() + i);
	}

	// selectNoK返回的就是第10小的元素的值
	int pos = selectNoK(vec, 0, vec.size() - 1, 10);
	cout << vec[pos] << endl; // 第10小的
	// 如果要找前10小的,访问[0,pos]就是了

	// 额外的需求
	/*
	有一个大文件,里面放的是整数,内存限制200M,求最大的前10个
	采用的计算分治的思想了
	大致计算一下整数文件的大小 / 200M = 要分的小文件的数量

	哈希映射:  整数 % 小文件的个数 = file_index

	然后现在每一个小文件就可以加载到内存当中了,就可以对每一个小
	文件的整数求top k元素了,然后合并小文件结果即可
*/
	return 0;
}

C++的学习心得和知识总结 第十四章(完)_第22张图片

第六题:海量数据Top K系列问题和查重问题的综合应用

之前两节已经总结了 海量数据的两种问题场景:
在这里插入图片描述
本节主要讨论的是:海量数据重复次数最大/第k大的数据值是什么?
解法就是二种情形的综合考量:哈希表统计(使用映射表统计重复)+大小根堆/快排分割函数。联合求解问题。

	/*
	问题描述:
	求vector容器中元素重复次数最大的第10个的元素值  前10多的元素值
	*/
	srand((unsigned)time(nullptr));
	vector<int> vec;
	for (int i = 0; i < 100; ++i)
	{
		vec.push_back(rand()%100 + 1);
	}

这里要求的是 重复次数最大的第10 或者 前十多的数据值。
重复次数的统计如下:

unordered_map<int, int>mymap;//进行数据的统计
	/* 
	【】运算符重载函数
	拿val数字在map中查找,如果val不存在,numMap[val]会插入一个[val, 0]
	返回其值0的引用。这么一个返回值,然后++,得到一个[val, 1]这么一组新数据
	如果val存在,numMap[val]刚好返回的是val数字对应的second重复的次数,
	直接++,即可。
	*/
	for (int val : vec)
	{
		mymap[val]++;
	}

于是就把上面的数据源的全部数据和其重复次数都求出来了。
然后就需要在此基础之上,求出重复次数最多的k大,或者第K。
我们选择 小根堆进行查找。(当然这里使用快排分割也行,只是我需要的不仅仅是重复次数的排名,我还需要保存一下 重复次数最多的值 是谁?)
C++的学习心得和知识总结 第十四章(完)_第23张图片
或者如下:

//这里需要的是一个小根堆类型  数字和重复的次数
	typedef priority_queue<pair<int, int>, vector<pair<int, int>>,
		function<bool(pair<int, int>&, pair<int, int>&)>>MinHeap;

	//自定义小根堆的比较方式
MinHeap minheap([](pair<int, int>& src1, pair<int, int>& src2)->bool
	{
		return src1.second > src2.second;
	});

如上:priority_queue 默认是一个大根堆,所以需要重新定义小根堆结构。自定义这个priority_queue,就是为了priority_queue的元素比较方式。priority_queue的第一个参数是元素类型,第二个参数是priority_queue 底层所依赖的容器的类型。(在数组上构建一个堆,才是可行的。毕竟是通过下标的相对位置来表示的)在第三个参数上(函数对象类型),需要传入一个函数对象(自定义小根堆的比较方式)。function函数对象类型接收bool(pair&,pair&)的类型函数,我们没必要去定义一个类型,这里函数对象类型接收传入lambda表达式。

MinHeap minheap([](pair<int, int>& src1, pair<int, int>& src2)->bool
	{
		return src1.second > src2.second;
	});

这句话 是在定义小根堆对象,然后传入一个函数对象来指导编译器进行pair对象元素的大小比较方式。因为pair类型的比较方式是通过key进行的,但是这里需要的是拿val(重复次数)进行比较,然后key只是作为数值出现。

int main()
{
	/*
	问题描述:
	求vector容器中元素重复次数最大的第10个的元素值  前10多的元素值
	*/
	srand((unsigned)time(nullptr));
	vector<int> vec;
	for (int i = 0; i < 100; ++i)
	{
		vec.push_back(rand()%100 + 1);
	}

	unordered_map<int, int>mymap;//进行数据的统计
	/* 
	拿val数字在map中查找,如果val不存在,numMap[val]会插入一个[val, 0]
	返回其值0的引用。这么一个返回值,然后++,得到一个[val, 1]这么一组新数据
	如果val存在,numMap[val]刚好返回的是val数字对应的second重复的次数,
	直接++,即可。
	*/
	for (int val : vec)
	{
		mymap[val]++;
	}
	//统计结束
	//这里需要的是一个小根堆类型  数字和重复的次数
	typedef priority_queue<pair<int, int>, vector<pair<int, int>>,
		function<bool(pair<int, int>&, pair<int, int>&)>>MinHeap;

	//自定义小根堆的比较方式
	MinHeap minheap([](pair<int, int>& src1, 
	pair<int, int>& src2)->bool
		{
			return src1.second > src2.second;
		});
	//找前10 重复次数多的元素值
	// 先从map表中读10个数据到小根堆中,建立top 10的小根堆,最小的元素在堆顶
	unordered_map<int, int>::iterator it = mymap.begin();
	for (int i = 0;i < 10 && it!=mymap.end(); ++i,++it)
	{
		minheap.push(*it);
	}
	//继续遍历剩余的元素
	for (; it != mymap.end(); ++it)
	{
		//如果map表中当前元素重复次数大于堆顶元素的重复次数,则替换
		if (it->second > minheap.top().second)
		{
			minheap.pop();
			minheap.push(*it);
		}
	}
	// 堆中剩下的就是重复次数最大的前k个
	while (!minheap.empty())
	{
		//auto& pair = minheap.top();
		const pair<int, int>& pair = minheap.top();
		cout << pair.first << " : " << pair.second << endl;
		minheap.pop();
	}
	return 0;
}

C++的学习心得和知识总结 第十四章(完)_第24张图片
新的需求来了:
在这里插入图片描述
常用解法:
在这里插入图片描述

#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
/*
快排分割函数,选择arr[i]号元素作为基数,把小于arr[i]的元素
调整到左边,把大于arr[i]的元素调整到右边并返回基数位置的下标
*/


int main()
{
	/*
	问题描述:
	有一个大文件,内存限制200M,求文件中重复次数最多的前10个
	大文件 =》 小文件
	大文件里面的数据 =》 哈希映射 =》 把数据离散的放入小文件当中
	大文件划分小文件(哈希映射)+ 哈希统计 + 小根堆(快排分割)

	这里如果使用快排分割,比小根堆块,毕竟不需要把全部数据
	都给遍历一遍
	*/
	srand((unsigned)time(nullptr));
	const int DataSize = 500;//数据量 对内存有限制
	int* filebuffer = new int[DataSize];//开辟缓冲区
	FILE* pfile = fopen("data.txt", "wb");
	if (pfile == nullptr)
		return -1;
	for (int i = 0; i < DataSize; ++i)
	{
		filebuffer[i] = rand()%100  + 1;
	}
	fwrite(filebuffer, sizeof(int), DataSize, pfile);
	fclose(pfile);
	delete[]filebuffer;

	//打开源数据文本大文件
	FILE* Big_file_ptr = fopen("data.txt", "rb");
	if (Big_file_ptr == nullptr)
		throw"this file is empty";
	//下面是进行大文件的数据映射切割
	const int small_file_nuber = 3;//小文件个数
	FILE* small_file_array[small_file_nuber] = { nullptr };
	for (int i = 0; i < small_file_nuber; ++i)
	{
		//存放小文件的文件名
		char small_file_name[20];
		//给小文件起名字
		sprintf(small_file_name, "data%d.txt", i + 1);
		//给文件数组每一个文件初始化指针
		small_file_array[i] = fopen(small_file_name, "wb+");
	}
	//下面是进行 哈希映射 数据分流
	int data;//每次读一个
	while (fread(&data, sizeof(int), 1, Big_file_ptr)>0)
	{
		//哈希映射
		int data_index = data % small_file_nuber;
		//向对应的文件里面写入数据
		fwrite(&data, sizeof(int), 1, small_file_array[data_index]);
	}
	///////////////////////////////////////////////////////////////
	// 定义一个链式哈希表
	unordered_map<int, int>mymap;//进行数据的统计
	
	//这里需要的是一个小根堆类型  数字和重复的次数
	typedef priority_queue<pair<int, int>, vector<pair<int, int>>,
		function<bool(pair<int, int>&, pair<int, int>&)>>MinHeap;

	//自定义小根堆的比较方式
	MinHeap minheap([](pair<int, int>& src1,
	 pair<int, int>& src2)->bool
		{
			return src1.second > src2.second;
		});
	//找前10 重复次数多的元素值
	// 先从map表中读10个数据到小根堆中,建立top 10的小根堆,最小的元素在堆顶
	// 分文件求解小文件的top 10大的数字,并求出最终结果
	for (int i = 0; i < small_file_nuber; ++i)
	{
		// 恢复小文件的文件指针到起始位置
		fseek(small_file_array[i], 0, SEEK_SET);

		//接下来做的第一件事就是在这个小文件里面,统计元素出现的次数
		while ((fread(&data, sizeof(int),1,small_file_array[i]) > 0))
		{
			mymap[data]++;//统计当前小文件里面的数据
		}
		//再接下来做的一件事就是 根据上面的mymap 构建小根堆进行查找
		unordered_map<int, int>::iterator it = mymap.begin();
	//下面的if判断非常重要,不然的话3个文件 每个一次,这样mymap的容量就是30个
		if (minheap.empty())// 如果堆是空的,先往堆方10个数据
		{
	// 先从map表中读10个数据到小根堆中,建立top 10的小根堆,最小的元素在堆顶
			for (int i = 0; i < 10 && it != mymap.end(); ++i, ++it)
			{
				minheap.push(*it);
			}
		}
		
		//继续遍历剩余的元素
		for (; it != mymap.end(); ++it)
		{
			//如果map表中当前元素重复次数大于堆顶元素的重复次数,则替换
			if (it->second > minheap.top().second)
			{
				minheap.pop();
				minheap.push(*it);
			}
		}
		
		// 清空哈希表,进行下一个小文件的数据统计
		mymap.clear();
	}
	for (int i = 0; i < small_file_nuber; ++i)//关闭这3个小文件
	{
		fclose(small_file_array[i]);
	}

	// 堆中剩下的就是重复次数最大的前k个
	while (!minheap.empty())
	{
		auto& pair = minheap.top();
		cout << pair.first << " : " << pair.second << endl;
		minheap.pop();
	}

	return 0;
}

C++的学习心得和知识总结 第十四章(完)_第25张图片
C++的学习心得和知识总结 第十四章(完)_第26张图片
注:vector采用clear()函数的时候只能清除元素,并不能清除内存,而要清除内存则可以用swap()。

vector<int> v;
for(size_t i = 0; i < 100; ++i)
{
        v.push_back(i);
 }
cout << "size=" << v.size() << " capacity=" << v.capacity() << endl; 

 v.clear();
cout << "After clear(): " << v.size() << " " << v.capacity() << endl;

vector<int> (v).swap(v);
cout << "After swap(): " << v.size() << " " << v.capacity() << endl;

结果如下:

size=100 capacity=128
After clear(): 0 128
After swap(): 0 0

这里的capacity是指vector v的容量,也就是占用的内存,最开始之所以容量是128而不是100,是因为vector在push_back的时候容量是按照2的指数增长的。当调用clear()函数之后,虽然size变成了0,但是其占据的内存并没有变,调用swap函数之后才能彻底的清除。

你可能感兴趣的:(C++的学习心得和知识总结)