一.大数加减法
1.题目
由于C++的内置类型存储的数字大小有限,需要使用字符串存储,实现这种情况下的大数据加减法:
cpp
class BigInt
{
public:
BigInt(string str):strDigit(str){}
private:
string strDigit;
friend ostream& operator<<(ostream& out, const BigInt& src);
friend BigInt operator+(const BigInt& lrc, const BigInt& src);
friend BigInt operator-(const BigInt& lrc, const BigInt& src);
};
2.代码实现
cpp
class BigInt
{
public:
BigInt(string str):strDigit(str){}
private:
string strDigit;
friend ostream& operator<<(ostream& out, const BigInt& src);
friend BigInt operator+(const BigInt& lrc, const BigInt& src);
friend BigInt operator-(const BigInt& lrc, const BigInt& src);
};
//前导零处理函数
void eraseFrontZero(string& s)
{
for (auto i = s.begin(); i < s.end();)
{
if (*i == '0')
{
i = s.erase(i);
}
else if (*i == '-')
{
++i;
}
else
{
break;
}
}
}
ostream& operator<<(ostream& out, const BigInt& src)
{
out << src.strDigit << endl;
return out;
}
BigInt operator+(const BigInt& lrc, const BigInt& src)
{
//处理一方为负数的情况
if (lrc.strDigit[0] == '-'&&src.strDigit[0]!='-')
{
string lrcCopy = lrc.strDigit;
lrcCopy.erase(lrcCopy.begin());
return operator-(src, BigInt(lrcCopy));
}
else if (src.strDigit[0] == '-'&&lrc.strDigit[0]!='-')
{
string lrcCopy = src.strDigit;
lrcCopy.erase(lrcCopy.begin());
return operator-(lrc, BigInt(src));
}
string result="";//定义的所有变量最好都初始化一下,字符串可以初始化为空
bool flag = false;//用于进位标识
string slrc = lrc.strDigit;
string ssrc = src.strDigit;
eraseFrontZero(slrc);
eraseFrontZero(ssrc);
int i = slrc.size()-1;
int j = ssrc.size()-1;
//统一处理同正与同负的情况
if ((lrc.strDigit[0] != '-'&&src.strDigit[0]!='-')|| (lrc.strDigit[0] == '-' && src.strDigit[0] == '-'))
{
if (lrc.strDigit[0] == '-' && src.strDigit[0] == '-')
{
slrc.erase(slrc.begin());
ssrc.erase(ssrc.begin());
--i;
--j;
}
for (; i >= 0 && j >= 0; --i, --j)
{
int ret = slrc[i] - '0' + ssrc[j] - '0';//利用字符的ASCII值
if (flag)
{
ret += 1;
flag = false;
}
if (ret >= 10)
{
ret %= 10;
flag = true;
}
//也可以使用to_string方法,但是这个方法涉及临时对象的创建,效率相对较慢。
result.push_back(ret+'0');//ret为整数,需要转化为字符
}
//处理多出来的位
if (i > j)
{
for (; i >= 0; --i)
{
int ret = slrc[i]-'0';
if (flag)
{
ret += 1;
flag = false;
}
if (ret >= 10)
{
ret %= 10;
flag = true;
}
result.push_back(ret + '0');
}
}
else if(i<j)
{
for (; j >= 0; --j)
{
int ret = ssrc[j]-'0';
if (flag)
{
ret += 1;
flag = false;
}
if (ret >= 10)
{
ret %= 10;
flag = true;
}
result.push_back(ret + '0');
}
}
if (flag)
{
result.push_back('1');
}
//如果是两个负数,在前面加上负号
if (lrc.strDigit[0] == '-' && src.strDigit[0] == '-')
{
result += "-";
}
reverse(result.begin(), result.end());//由于result中数字顺序是反的,颠倒
//这里也可以直接返回result,编译器会自己检查BigInt类是否存在接收一个string的构造函数
return BigInt(result);
}
}
BigInt operator-(const BigInt& lrc, const BigInt& src)
{
//处理一方为负数的情况
if (lrc.strDigit[0] == '-'&&src.strDigit[0]!='-')
{
string lrcCopy = src.strDigit;
lrcCopy.insert(lrcCopy.begin(), '-');
return operator+(lrcCopy, BigInt(lrc.strDigit));
}
else if (src.strDigit[0] == '-'&&lrc.strDigit[0]!='-')
{
string lrcCopy = src.strDigit;
lrcCopy.erase(lrcCopy.begin());
return operator+(lrc, BigInt(src));
}
string result = "";
string max=lrc.strDigit;
string min=src.strDigit;
eraseFrontZero(max);
eraseFrontZero(min);
bool flag = false;
bool minor = true;//记录最后的结果是正是负
int i = lrc.strDigit.size() - 1;
int j = src.strDigit.size() - 1;
if ((lrc.strDigit[0] != '-' && src.strDigit[0] != '-') || (lrc.strDigit[0] == '-' && src.strDigit[0] == '-'))
{
//找出最大,最小,永远使用最大减最小
if (i < j)
{
max = src.strDigit;
min = lrc.strDigit;
swap(i, j);
minor = false;
}
else if (i == j)
{
if (lrc.strDigit < src.strDigit)
{
max = src.strDigit;
min = lrc.strDigit;
swap(i, j);
minor = false;
}
else if(lrc.strDigit==src.strDigit)
{
return BigInt("0");
}
}
//处理同负的情况
if (max[0] == '-' && min[0] == '-')
{
max.erase(max.begin());
min.erase(min.begin());
--i;
--j;
}
for (; i >= 0 && j >= 0; --i, --j)
{
int ret = (max[i] - '0') - (min[j] -'0');
if (flag)
{
ret -= 1;
flag = false;
}
if (ret < 0)
{
ret=ret+10;
flag = true;
}
result.push_back(ret + '0');
}
//因为我们恒保持i对应的字符串大于j,所以这里可以不用考虑j
if (i >=0)
{
for (; i >= 0; --i)
{
int ret = max[i]-'0';
if (flag)
{
ret -= 1;
flag = false;
}
if (ret < 0)
{
ret =-ret;
flag = true;
}
result.push_back(ret + '0');
}
}
//处理同负的情况
if (lrc.strDigit[0] == '-' && src.strDigit[0] == '-')
{
minor = !minor;
}
if (!minor)
{
result += '-';
}
reverse(result.begin(), result.end());
//处理最后结果的前导零
eraseFrontZero(result);
return BigInt(result);
}
}
注意三种情况:
1.加数,减数中存在负号的需要特殊处理。
2.加数,减数存在前导零的情况。
3.总结
1.定义的所有变量最好都初始化一下,字符串可以初始化为空
2.整数字符强转为整数会得到其ASCII值,无法得到其本身的整数,正确的做法是将整数字符减去字符零。
3.to_string方法涉及临时对象的构造与拷贝,效率较低,可看看能否采用其他方法,如push_back方法能够进行隐式的类型转化。
4.标准库算法:reverse能够颠倒字符串的顺序,但是只能适用于整数字符,字母字符以及常见特殊符号字符。
5.返回类时,可以直接返回类内定义的构造函数的参数,编译器会自动调用改构造函数进行构造,但是不推荐这种写法。
二.迷宫路径问题
1.深度优先遍历(DFS)
1.题目
利用深度优先遍历解决:根据用户输入的迷宫的行和列,还有具体的迷宫(由0/1组成的二维数组),1表示不能走,0表示能走,遍历这个迷宫,找到一条从左上角到右下角的路径,存在返回这个二维数组并将路径置成 * ,没找到返回信息说明不存在一条路径。
2.代码实现
cpp
class Node
{
public:
Node(int x=0, int y=0, int val=0)
:_x(x)
, _y(y)
, _val(val)
{
//初始将state数组全部置成1,可以解决超出二维数组的边界问题
//0->右,1->下,2->左,3->上
for (int i = 0; i < 4; ++i)
_state[i] = 1;
}
int _x;
int _y;
int _val;
int _state[4];//用于记录这个节点的四周是否可以走
};
class Maze
{
public:
//析构函数
~Maze()
{
//注意二维数组的析构方式,与开辟方式相反
for (int i = 0; i < _row; ++i)
{
delete[] p[i];
}
delete[]p;
p = nullptr;
}
//创建二维数组
void makeVector()
{
cout << "请输入迷宫的行和列:" << endl;
cin >> _row;
cin >> _col;
//注意这里的堆区二维数组的开辟方式
p = new Node*[_row];
for (int i = 0; i < _row; ++i)
{
p[i] = new Node[_col];
}
cout << "请输入迷宫的路径信息(1表示不可以走,0表示可以):" << endl;
for (int i = 0; i < _row; ++i)
{
for (int j = 0; j < _col; ++j)
{
int val;
cin >> val;
p[i][j] = Node(i, j, val);
}
}
}
//初始化二维数组的state
void initializeState()
{
for (int i = 0; i < _row; ++i)
{
for (int j = 0; j < _col; ++j)
{
if (p[i][j]._val == 0)
{
//只有该节点的值为0,才有必要设置其state
if (i+1<_row&&p[i + 1][j]._val==0)
{
p[i][j]._state[1] = 0;
}
if (j + 1 < _col && p[i][j + 1]._val == 0)
{
p[i][j]._state[0] = 0;
}
if (j - 1 > 0 && p[i][j - 1]._val == 0)
{
p[i][j - 1]._state[2] = 0;
}
if (i - 1 > 0 && p[i - 1][j]._val == 0)
{
p[i - 1][j]._state[3] = 0;
}
}
}
}
}
//深度优先搜索遍历迷宫路径方法
Node** searchMaze()
{
stack<Node>stk;//栈辅助深度优先搜索
if (p[0][0]._val == 1)
return nullptr;
stk.push(p[0][0]);
while (!stk.empty())
{
//注意这里必须使用引用,否则会调用Node的默认拷贝构造函数,发生拷贝,导致这里修改top不影响stk.top()
Node& top = stk.top();
if (top._x == _row-1 && top._y == _col-1)
{
while (!stk.empty())
{
top = stk.top();
//这里赋值会将*转化为其ASCII值42
p[top._x][top._y]._val = '*';
stk.pop();
}
return p;
}
if (top._state[0] == 0)
{
top._state[0] = 1;
Node node= p[top._x][top._y + 1];
node._state[2] = 1;
stk.push(node);
continue;
}
if (top._state[1] == 0)
{
top._state[1] = 1;
Node node = p[top._x+1][top._y];
node._state[3] = 1;
stk.push(node);
continue;
}
if (top._state[2] == 0)
{
top._state[2] = 1;
Node node = p[top._x][top._y-1];
node._state[0] = 1;
stk.push(node);
continue;
}
if (top._state[3] == 0)
{
top._state[3] = 1;
Node node = p[top._x-1][top._y];
node._state[1] = 1;
stk.push(node);
continue;
}
stk.pop();
}
//走到死路出栈回退
return nullptr;
}
/*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*/
int _row;
int _col;
Node** p;
};
int main()
{
Maze m;
m.makeVector();
m.initializeState();
Node** p =m.searchMaze();
if (p == nullptr)
{
cout << "不存在一条迷宫路径" << endl;
return 0;
}
for (int i = 0; i < m._row; ++i)
{
for (int j = 0; j < m._col; ++j)
{
if ((char)p[i][j]._val == '*')
{
cout << '*' << " ";
}
else
cout << p[i][j]._val << " ";
}
cout << endl;
}
cout << endl;
}
3.总结
1.二维数组的堆区开辟,析构方式。
2.Node& top = stk.top();这里可以使用引用规避拷贝构造(必须使用引用,不能通过提供深拷贝构造函数来解决这一问题,深拷贝解决的是拷贝是否独立的问题)。
3.最好给Maze类提供一个构造函数,将开辟二维数组空间的事交给构造函数处理。
2.广度优先遍历(BFS)
广度优先遍历相对于深度优先遍历能够找到最短路径,深度优先遍历只能找到一条路径。
1.题目
利用深度优先遍历求最短路径
2.代码实现
cpp
class Node
{
public:
Node(int x = 0, int y = 0, int val = 0)
:_x(x)
, _y(y)
, _val(val)
{
for (int i = 0; i < 4; ++i)
_state[i] = 1;
}
int _x;
int _y;
int _val;
int _state[4];//用于记录这个节点的四周是否可以走
};
class Maze
{
public:
//析构函数
~Maze()
{
for (int i = 0; i < _row; ++i)
{
delete[] p[i];
}
delete[]p;
p = nullptr;
}
//创建二维数组
void makeVector()
{
cout << "请输入迷宫的行和列:" << endl;
cin >> _row;
cin >> _col;
//注意这里的堆区二维数组的开辟方式
p = new Node * [_row];
for (int i = 0; i < _row; ++i)
{
p[i] = new Node[_col];
}
cout << "请输入迷宫的路径信息(1表示不可以走,0表示可以):" << endl;
for (int i = 0; i < _row; ++i)
{
for (int j = 0; j < _col; ++j)
{
int val;
cin >> val;
p[i][j] = Node(i, j, val);
}
}
}
//初始化二维数组的state
void initializeState()
{
for (int i = 0; i < _row; ++i)
{
for (int j = 0; j < _col; ++j)
{
//只有该节点的值为0,才有必要设置其state
if (p[i][j]._val == 0)
{
if (i + 1 < _row && p[i + 1][j]._val == 0)
{
p[i][j]._state[1] = 0;
}
if (j + 1 < _col && p[i][j + 1]._val == 0)
{
p[i][j]._state[0] = 0;
}
if (j - 1 > 0 && p[i][j - 1]._val == 0)
{
p[i][j - 1]._state[2] = 0;
}
if (i - 1 > 0 && p[i - 1][j]._val == 0)
{
p[i - 1][j]._state[3] = 0;
}
}
}
}
}
//广度优先搜索遍历迷宫路径方法
Node** searchMaze()
{
if (p[0][0]._val == 1)
return nullptr;
queue<Node>que;//辅助广度优先遍历
vector<Node>vec;//辅助记录路径信息
vec.resize(_row * _col);
que.push(p[0][0]);
while (!que.empty())
{
Node& front = que.front();
que.pop();
//遍历找出路径赋值
if (front._x == _row - 1 && front._y == _col - 1)
{
Node node= vec[front._x * _col + front._y];
//由于数组中最后存的是前一个节点,所以最后一个节点需要单独处理
p[front._x][front._y]._val = '*';
while (1)
{
p[node._x][node._y]._val = '*';
node = vec[node._x*_col+node._y];
if (node._x == 0 && node._y == 0)
return p;
}
}
if (front._state[0] == 0)
{
front._state[0] = 1;
Node node = p[front._x][front._y + 1];
node._state[2] = 1;
vec[front._x*_col+front._y + 1] = front;
que.push(node);
}
if (front._state[1] == 0)
{
front._state[1] = 0;
Node node = p[front._x + 1][front._y];
node._state[3] = 1;
vec[(front._x+1) * _col + front._y] = front;
que.push(node);
}
if (front._state[2] == 0)
{
front._state[2] = 0;
Node node = p[front._x][front._y - 1];
front._state[0] = 0;
vec[front._x * _col + front._y - 1] = front;
que.push(node);
}
if (front._state[3] == 0)
{
front._state[3] = 0;
Node node = p[front._x - 1][front._y];
front._state[1] = 0;
vec[(front._x-1) * _col + front._y] = front;
que.push(node);
}
}
return nullptr;
}
/*0 0 1 1 1 1
1 0 0 0 0 1
1 0 1 1 0 1
1 1 0 0 0 1
1 0 1 1 1 1
1 0 0 0 0 0*/
int _row;
int _col;
Node** p;
};
int main()
{
Maze m;
m.makeVector();
m.initializeState();
Node** p = m.searchMaze();
if (p == nullptr)
{
cout << "不存在一条迷宫路径" << endl;
return 0;
}
for (int i = 0; i < m._row; ++i)
{
for (int j = 0; j < m._col; ++j)
{
if ((char)p[i][j]._val == '*')
{
cout << '*' << " ";
}
else
cout << p[i][j]._val << " ";
}
cout << endl;
}
cout << endl;
}
3.总结
1.由于队列每次遍历都要出队一个节点,导致队列无法记录路径信息,我们需要开辟额外的数组来记录路径信息。
2.记录数组的设计方式是将二维数组上的每一个节点映射到一维数组上,除0号位置外,其他位置都记录前一个节点(即记录它是从哪里来的),最后遍历是不断向前遍历赋值。
3.对于非最短路径,在遍历的过程中无法到到迷宫的出口节点。
三.大数据查重与Top k问题
1.大数据查重
(1)哈希表(无内存限制)
如:无内存限制查找所有重复的元素及其重复次数
cpp
int main()
{
const int SIZE = 10000;
unordered_map<int, int>mp;
int arr[SIZE]{0};
for (auto& v : arr)
{
v = rand();
}
for (auto& v : arr)
{
mp[v]++;
}
for (auto& pair : mp)
{
if(pair.second>1)
cout << pair.first << " : " << pair.second << endl;
}
}
(2)哈希表(有内存限制)
哈希表虽然查找速度快,但是其内存占用率较高,例如:我们想要从一个存放了50亿个整数的文件中,查找出所有重复的元素及其重复次数,限制使用400M的内存,如果直接使用哈希表存储的话需要40G的内存(10亿个字节大概占1G),显然不满足题意,所以为了解决这一问题,我们需要用到分治思想。
对于这一问题核心思路是,将大文件拆分为127个小文件(因为要使用哈希函数,所以这里的文件最好是素数,能够将大文件更平均的散列到小文件中,40G/400M=100,这里我们选取的小文件数量要大于100,因为可能散列不均衡),然后遍历每一个大文件中的数据,使用哈希函数(除留余数法),将每一个数据散列到一个小文件中(相同的数据必定在同一个文件中),然后分别对每一个小文件使用哈希表直接存储即可,最后将结果合并。
其他 :解决这类问题,我们还可以使用布隆过滤器,对于特殊的字符串类型,我们还可以使用字典树(前缀树)。
2.大数据解决Top k问题
同样包括有内存限制的与无内存限制的,无内存限制的同样使用分治思想将大文件拆分为小文件,不再赘述,对于Top k问题本身,有两种解法:
(1)优先级队列
优先级队列时间复杂度为O(n)。
cpp
//求最小的前10个
int main()
{
const int SIZE = 10000;
priority_queue<int,vector<int>,greater<int>>que;
int arr[SIZE]{0};
for (auto& v : arr)
{
v = rand();
}
for (int i=0;i<10;++i)
{
que.push(arr[i]);
}
for (int i = 10; i < SIZE; ++i)
{
if (arr[i] < que.top())
{
que.pop();
que.push(arr[i]);
}
}
while(!que.empty())
{
cout << que.top()<<endl;
que.pop();
}
}
(2)快排分割
最好时间复杂度为O(logn),但是如果元素趋于有序,时间复杂度可能会退化为O(n^2),所以两种方法的平均时间复杂度基本相同。
cpp
//求最大的前5个
int Partaion(int arr[], int begin, int end)
{
int i = begin;
int j = end;
int val = arr[i];
while (i < j)
{
while (i < j && arr[j] < val)
j--;
if (i < j)
{
arr[i] = arr[j];
i++;
}
while (i < j && arr[i] > val)
i++;
if (i < j)
{
arr[j] = arr[i];
j--;
}
}
arr[i] = val;
return i;
}
void SelectTopk(int arr[], int begin, int end, int k)
{
int pos = Partaion(arr, begin, end);
if (pos == k - 1)
{
return;
}
else if (pos > k - 1)
{
SelectTopk(arr, begin, pos - 1, k);
}
else
{
SelectTopk(arr, pos + 1, end, k);
}
}
3.大数据查重与Top k问题的综合应用
将两者问题结合起来,如问:找出重复次数最多的前k个元素,同样包括有内存限制与无内存限制两种。
cpp
//查找重复次数最多的前5个元素
int main()
{
const int SIZE = 1000;
unordered_map<int, int>mp;
priority_queue<pair<int, int>,vector<pair<int,int>>,less<pair<int,int>>>que;
int k = 5;
int arr[SIZE]{ 0 };
for (auto& v : arr)
{
v = rand()%100+1;
}
for (auto& v : arr)
{
mp[v]++;
}
auto it = mp.begin();
for (int i=0;i<5;++i,++it)
{
que.push({it->second,it->first});
}
for (int i = 5; i < mp.size(); ++i, ++it)
{
if (it->second > que.top().first)
{
que.pop();
que.push({it->second,it->first});
}
}
while (!que.empty())
{
cout << que.top().second << " : " << que.top().first << endl;
que.pop();
}
}