本文内容整理于胡凡老师的《算法笔记》
1 配置VS环境
1.1 高版本默认不能使用scanf函数
配置:项目->属性->C/C+±>预处理器->预处理器定义->输入:_CRT_SECURE_NO_WARNINGS;
1.2 输入齐全的头文件
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#include<stack>
#include<fstream>
#include<sstream>
using namespace std;
2 C++基本语法易忘点整理
- scanf和printf 比 cin和cout快的多;不要同时在一个程序中使用cout和printf
- %d:int型;
%lld:long long型
%f:(printf)float和double型;(scanf)float
%lf:(scanf)double型
%c:char型;能够识别空格和换行并将其输入
%s:字符串型;通过空格或换行来识别一个字符串的结束 - 整型:
(1)int型:32位,绝对值在10的9次方范围以内的整数
(2)long long型:64位,数值范围超过10的10次方 - 不要使用float,碰到浮点数的数据都应该用double来存储
- 小写字母比大写字母的ASCII码值大32
- printf("%c", 7); ASCII码为7是控制响铃功能的控制字符
- 无穷大常用设置:
const int INF = (1<<30) - 1;
const int INF = 0x3fffffff;
const int inf = 1000000000;(8个0) - &:取地址运算符
- scanf中输入char数组,不需要加&,因为数组名称本身代表数组第一个元素的地址
- 输入格式样式:"%d,%d,%d"可接受 a,b,c 这样的输入
"%d:%d:%d"可接受 a🅱️c这样的输入
"%d%d%d"可接受用空格隔开的输入 - scanf的%c格式是可以读入空格和换行符。
- 输出格式:
%.mf:保留m位小数输出;
%md:右对齐输出,不足m位则高位补空格,否则不变输出
%0md:不同%md是,高位补0 - getchar()输入单个字符;putchar()输出单个字符;
gets_s用来输入一行字符串,识别换行符作为输入结束
puts用来输出一行字符串,并紧跟一个换行 - math函数
fabs(double x):double型变量取绝对值
floor(double x)和ceil(double x):向下取整和向上取整
log(double x):以自然对数为底的对数
round(double x):四舍五入 - 如果数组大小较大(大概10的6次方级别),则需要将其定义在主函数外面,否则会使程序异常退出,函数外面申请的全局变量来自静态存储区
- memset:对所有数组赋值相同的0或-1(添加头文件cstring)
- 字符数组注意事项
(1)结束符\0的ASCII码为0,即空字符NULL,占用一个字符位,因此开字符数组的时候
千万记得字符数组的长度一定要比实际存储字符串的长度至少多1;
(2)使用getchar输入字符串时,记得在每个字符串后面加上“\0” - ASCII码:
09:4857;
AZ:6590;
az:97122; - string.h头文件函数
(1)strlen():可以得到字符串长度
(2)strcmp():按字典顺序比较两个字符串
(3)strcpy(字符数组1, 字符数组2):将数组2复制给数组1
(4)strcat():连接两个字符串
(5)sscanf(str, “%d”, &n); 将字符数组str中的内容以"%d"的格式写到n中
(6)sprintf(str, “%d”, n); n以"%d"的格式写到str字符数组中
举例1:将字符数组str中的内容按"%d:%lf,%s"的格式写到int型变量n,double型变量db,
char型数组str2中
char str[100] = “2048:3.14,hello”, str2[100];
sscanf(str, “%d:%lf,%s”, &n, &db, str2);
举例2:将int型变量n,double型变量db,char型数组str2按照"%d:%lf,%s"的格式写入字符数组str中
sprintf(str, “%d:%lf,%s”, n, db, str2); - 指针:
变量的地址:&a
定义指针变量并初始化:int *pa = &a;
取值:b = *pa; - 引用,实参设置为int &x:则改变值则会影响函数外
- 多点测试
(1)while…EOF型
while(scanf("%d", &n) != EOF){…}
其中EOF为-1,效果为无限输入处理
ps:whle(gets_s(str))同样可以
(2)while…break型
while(scanf("%d%d", &a, &b), a||b){} 直到输入的a和b同时为0,才结束while
(3)while(T-)型
3 C++标准模板库STL
3.1 vector向量:“边长数组”
(1) vector定义
vector<typename> name;
//例如
vector<int> name; //int型的一维可变数组
vector<vector<int> > name; //int型的二维可变数组
vector<int> vi[100]; //int型的二维数组,其中一维是固定为100大
(2) vector元素访问:
a、 通过下标访问(类似数组):vi[index],下标从0到vi.size()-1
b、 通过迭代器访问:
迭代器(类似指针)定义:
vector<typename>::iterator it = vi.begin(); //begin()为vi的首元素地址
访问元素: *(it + i) //等同于vi[i]; ==
注:it支持自增自减,循环条件只能用it != vi.end()== //end()为尾元素的下一个地址。
(3) Vector常用函数:(vector向量vi)
- vi.push_back(x):在vector后面添加一个元素x
- vi.pop_back():删除vector的尾元素
- vi.size():获得元素个数
- vi.clear():清空所有元素
- insert(it, x):向指定位置的迭代器it处插入一个元素x
- erase(it):删除迭代器为it处的元素;
- erase(first, last):删除[first, last)的所有元素(不包含last位置元素)
3.2 set集合:元素自动去重并按升序排列(头文件#include)
(1) set定义:
set<typename> name;
(2) set元素访问: 只能通过迭代器访问:
set<typename>::iterator it = vi.begin();
注:除了vector和string外,STL容器都不支持*(it + i)的访问
(3) set函数:(set集合st)
- st.insert(x):将x插入集合中
- st.find(value):返回set中对应值为value的迭代器
- st.erase(it):it为所需要删除元素的迭代器
st.erase(value):value为所需要删除元素的值
st.erase(first, last):删除迭代器范围为[first, last)的元素 - st.size():得到元素个数
- clear():清空所有元素
3.3 string字符串
(1) string定义
string str = “abcd”;
(2) string元素访问:
- 下标访问,键入和打印string字符串,只能用cin和cout,或者使用str.c_str()转换为字符数组,才能用printf
- 通过迭代器访问:
string::iterator it = str.begin()
注:string和vector可以直接对迭代器进行加减某个数字,其他不行
(3) String函数
- +=:可以直接将两个string拼接起来
- 可以直接用符号比较,按照字典序
- str.length()或str.size()
- str.insert(pos, str2):在pos号位置插入字符串str2
str.insert(it, it2, it3):表示串[it2, it3)插在it的位置上 - str,erase(it):it为需要删除元素的迭代器
str.erase(first, last):删除元素的迭代器区间
str.erase(pos, length):pos为开始删除的起始位置,length为删除的字符个数 - str.clear():清空字符串
- str.substr(pos, len):返回从位置pos开始,长度为len的子串
- str.find(str2):当str2是str的子串时,返回其在str中第一次出现的位置,如果str2不是子串,则返回string::npos(值为-1或4294967295)
- str.find(str2, pos):从str的pos号位开始匹配str2
- str.replace(pos, len, str2):把str从pos号位开始,长度为len的子串替换成str2
- str.replace(it1, it2, str2):把str的迭代器[it1, it2)范围的子串替换为str2
3.4 queue队列:实现先进先出的容器
(1)queue定义:
queue<typename> name;
(2)queue元素访问
q.front()访问队首元素,q.back()访问队尾元素
(3)queue函数
- q.push(x):将x入队
- q.front(), q.back():分别获得队首和队尾元素
- q.pop():令队首元素出队
- q.empty():检查queue是否为空,返回true则为空
- q.size():返回队列内元素个数
3.5 priority_queue优先队列:队首元素一定是当前队列中优先级最大的
(1)priority_queue的定义:
priority_queue<typename> name;
(2)priority_queue元素访问:
只能用q.top()来访问队首元素
(3)priority_queue函数:
q.push(); q.top(); q.pop(); q.empty(); q.size();
(4)priority_queue元素优先级设定:
- 基本数据类型优先级设置:
定义:priority_queue<int, vector, less > name;
其中less 表示数字越大的优先级越大
换成greater表示数字越大的优先级越小 - 结构体的优先级设置:
例如:结构体fruit
方法一:重载小于号<: 使得队列按照从小到大排序,与sort中的规则相反 (使用引用效率更高),记得写在fruit结构体内
friend bool operator < (const fruit &f1, const fruit &f2)
{
return f1.price > f2.price;
}
则定义:priority_queue q;
方法二:写在结构体外
struct cmp{
bool operator () (fruit f1, fruit f2){
return f1.price > f2.price;
}
}
则定义:priority_queue<fruit, vector, cmp> q;
3.6 stack栈:实现后进先出的容器
(1)定义:
stack<typename> name;
(2)stack元素访问:
只能通过st.top()来访问栈顶元素
(3)stack函数:
st.push(x); st.top(); st.pop(); st.empty(); st.size();
注:stack中没有清空元素的操作,则可以:
while(!st.empty()){
st.pop();
3.7 algorithm中常用的函数:max,min,abs,reverse,fll,sort,lower_bound
- max(x, y); min(x, y); abs(x, y); swap(x, y);
- everse(it, it2):将数组指针在[it, it2)之间的元素或容器的迭代器内的元素反转
- fill(it, it2, value):将数组指针或容器迭代器在[it, it2)内的元素全部赋值为value;
- sort(首元素地址, 尾元素地址的下一个地址, 比较函数cmp(选填));sort默认递增排序
A. 基本数据类型数组的排序
定义从大到小排序
bool cmp(int a, int b){
return a > b;
}
注:>表示从大到小,<表示从小到大
B. 结构体数组的排序:举例结构体node
bool cmp(node a, node b){
return a.x > b.x;
}
C. STL容器:vector,string,deque
sort(it, it2, cmp)
- lower_bound(first, last, val):用来寻找在数组或容器[first, last)范围内第一个值大于等于val的元素的位置,返回的是指针或迭代器
- upper_bound(first, last, val):与上面不同的是,返回的是第一个值大于val,注:若没有找到元素,则返回可以插入该元素的位置的指针或迭代器。
3.8 map映射:类似于字典,将任何基本类型映射到任何基本类型(包括STL容器)(1) 需要引入头文件#include
- mao定义:
map<typename1, typename2> mp;
其中typename1相当于key的类型,typename2相当于value的类型
- map的元素访问:
A. 通过下标访问:
举例:map<char, int> mp;
mp[‘c’] = 20;
B. 通过迭代器访问:
map<typename1, typename2>::iterator it = mp.begin();
it -> first //表示当前映射的key
it -> second //表示当前映射的value
注:map会自动将键按照从小到大排序
- map函数:
- mp.find(key):返回键为key的映射的迭代器
- mp.erase(it):it为需要删除的元素的迭代器
mp.erase(key):key为要删除的映射的键
mp.erase(first, last) - mp.size(), mp.clear()
4 数据结构:树
4.1 链表:数据域与指针域
- 链表定义
struct node{
typename data;
node* next;
}
- 使用new为链表结点分配内存空间
- 基本用法:
typename* name = new typename
- 举例:
node* p = new node;
==注:使用完后记得释放内存:delete(p);
- 基本用法:
- 创建链表:举例
node* node1 = new node;
node* node2 = new node;
node1->data = 5;
node1->next = node2;
node2->data = 3;
node2->next = NULL;
- 静态链表:实现原理为hash,定义结构体数组,通过下标来定位
4.2 二叉树:根结点,左子树,右子树
- 概念:
- 根结点为第一层;
- 满足连通:边数等于顶点数减一的结构一定是一棵树
- 深度:从根结点(深度为1)自顶向下
- 高度:从最底层叶子结点(高度为1)自底向上
- 二叉树存储结构:
struct node{
typename data;
node* lchild, *rchild;
}
- 新建结点
node* newNode(int v)
{
node* Node = new node;
Node->data = v;
Node->lchild = Node->rchild = NULL;
return Node;
}
- 二叉树结点的查找和修改
void search(node* root, int x, int newData)
{
if(root == NULL)
return;
if(root->data == x)
{
root->data = newData;
return;
}
else
{
search(root->lchild, x, newData);
search(root->rchild, x, newData);
}
}
注意:如果函数中需要新建结点,则需要将函数参数中的node* root
改为node* &root
;如果只是修改当前已有结点的内容,则不需要加引用。
- 完全二叉树的存储结构
- 按照从上到下,从左到右进行编号(与层序遍历的序列相同)
- 存储的数组下标必须从1开始,下标1存放根结点
- 结点编号为x,则左孩子结点为2x,右孩子结点为2x+1
4.3 二叉树的遍历(递归程序)
- 先序遍历:根结点->左孩子->右孩子
void preOrder(node* root)
{
if(root == NULL)
return;
printf("%d ", root->data);
preOrder(root->lchild);
preOrder(root->rchild);
}
- 中序遍历:左孩子->根结点->右孩子
void inOrder(node* root)
{
if(root == NULL)
reutrn;
inOrder(root->lchild);
printf("%d ", root->data);
inOrder(root->rchild);
}
- 后序遍历:左孩子->右孩子->根结点
void postOrder(node* root)
{
if(root == NULL)
reutrn;
postOrder(root->lchild);
postOrder(root->rchild);
printf("%d ", root->data);
}
- 层序遍历
思路:将根节点存入队列中;取出队列的首结点,访问它;如果有左孩子,则将其入队;如果有右孩子,则将其入队;返回到第二步继续
void layerOrder(node* root)
{
queue<node*> q;
node* p = root;
q.push(p);
while(!q.empty())
{
p = q.front();
q.pop();
if(p->lchild != NULL) q.push(p->lchild);
if(p->rchild != NULL) q.push(p->rchild);
}
}
4.4 二叉树的遍历(非递归程序)
- 先序遍历
void preOrder(node* root)
{
node* p = root;
stack<Node*> st;
while(p || !st.empty())
{
while(p)
{
printf("%d ", p->data);
st.push(p);
p = p->lchild;
}
if(!st.empty())
{
p = st.top();
st.pop();
p = p->rchild;
}
}
}
- 中序遍历
void inOrder(node* root)
{
node* p = root;
stack<node*> st;
while(p || !st.empty())
{
while(p)
{
st.push(p);
p = p->lchild;
}
if(!st.empty())
{
p = st.top();
st.pop();
printf("%d ", p->data);
p = p->rchild;
}
}
}
- 后序遍历
思路:先一直遍历到最左边的结点,依次入栈;有一个标记指针,记录最近访问过的元素,只有当前结点满足条件:没有右孩子结点或者右孩子结点被访问过时才能够输出(被访问),否则,若访问了其左孩子结点,则转向访问右子树。
void postOrder(node* root)
{
node* p = root;
node* flag = NULL;
stack<node*> st;
while(p || !st.empty())
{
while(p)
{
st.push(p);
p = p->lchild;
flag = p;
}
if(!st.empty())
{
p = st.top();
if(p->rchild == NULL && p->rchild == flag)
{
printf("%d ", p->data);
st.pop();
flag = p;
p = NULL;
}
else
{
p = p->rchild;
flag = p;
}
}
}
}
4.5 先序和中序重建二叉树
注:中序序列可以与先序序列、后序序列、层序序列中的任意一个来构建二叉树,缺少中序序列就无法重建二叉树
思路:假设先序序列[preL, preR]
,其中当前子树的根结点为preL
,其中序序列区间为[inL, inR]
,遍历中序序列找到点:ink == preL
,此时对于先序序列来说,其左子树区间为[preL +1, preL + ink - inL]
,右子树区间为[preL + ink – inL + 1, preR]
,对于中序序列来说,其左子树区间为[inL, ink-1]
,其右子树区间为[ink+1, inR]
,利用递归的思路来解决,递归边界为preL > preR
,递归式为:
root->data = preSet[preL];
root->lchild = create(preL+1, preL + ink – inL, inL, ink - 1);
root->rchild = create(preL + ink – inL +1, preR, ink + 1, inR);
程序:
node* create(int preL, int preR, int inL, int inR)
{
//preSet[]为先序序列,inSet[]为中序序列
if (preL > preR)
return NULL;
node* root = new node;
root->data = preSet[preL];
int i;
for (i = inL; i <= inR; i++)
{
if (root->data == inSet[i])
break;
}
int numleft = i - inL;
root->lchild = create(preL + 1, preL + numleft, inL, i - 1);
root->rchild = create(preL + numleft + 1, preR, i + 1, inR);
return root;
}
另:给定中序序列和后序序列重建二叉树
node* create_1(int postL, int postR, int inL, int inR)
{
//postSet[]为后序序列
if (postL > postR)
return NULL;
node* root = new node;
root->data = postSet[postR];
int k;
for ( k = inL; k <= inR; k++)
{
if (root->data == inSet[k])
break;
}
int numleft = k - inL;
root->lchild = create_1(postL, postL + numleft - 1, inL, k - 1);
root->rchild = create_1(postL + numleft, postR - 1, k + 1, inR);
return root;
}
4.6 其他:静态二叉树,判断完全二叉树,左右结点对换
- 静态二叉树:利用结构数组的下标来替代指针的作用,例如堆,数组下标从1开始编号,2结点下标为其左孩子,2结点下标+1为其右孩子
- 判断二叉树是否为完全二叉树:利用层序遍历的算法,区别是无论其结点是否为NULL,都入队,当出队遇到NULL时,则判断队中是否还有非空指针,如有,则表示不是完全二叉树,反之则是。
- 将二叉树的左右结点对换:则利用后序遍历的思路
4.7 二叉查找树BST
- 定义:左子树上的所有结点的数据域都小于或等于根结点的数据域,右子树上的所有结点的数据域均大于根结点的数据域。(左子树<根结点<右子树)
- 新建结点:
node* newNode(int newData)
{
node* Node = new node;
Node->data = newData;
Node->lchild = Node->rchild = NULL;
return Node;
}
- 插入新结点==(记得使用引用符号&)==
void insert(node* &root, int newData)
{
if (root == NULL)
{
root = newNode(newData);
return;
}
if (newData == root->data) return;
else if (newData > root->data)
insert(root->rchild, newData);
else
insert(root->lchild, newData);
}
- 查找操作
void search(node* root, int x)
{
if (root == NULL)
{
printf("failed.\n");
return;
}
if (root->data == x)
printf("result: %d", root->data);
else if (root->data < x)
search(root->rchild, x);
else
search(root->lchild, x);
}
- 找到最小结点
node* findMin(node* root)
{
while (root->lchild != NULL)
{
root = root->lchild;
}
return root;
}
- 找到最大结点
node* findMax(node* root)
{
while (root->rchild != NULL)
{
root = root->rchild;
}
return root;
}
- 删除结点:先找到被删除结点的前驱结点(该结点的左子树中的最右结点)或者后驱结点(该结点的右子树中的最左结点),并用结点来代替被被删除结点的位置,然后同样的去删除前驱结点或后驱结点,直到删除到叶结点,则直接删除。利用递归函数,传入当前结点参数要用引用&。
void deleteNode(node*& root, int x)
{
if (root == NULL) return;
if (root->data == x)
{
if (root->rchild == NULL && root->lchild == NULL)
root = NULL;
else if (root->lchild != NULL)//有左孩子
{
node* pre = findMax(root->lchild);//前驱
root->data = pre->data;
deleteNode(root->lchild, pre->data);
}
else
{
node* next = findMin(root->rchild);//后驱
root->data = next->data;
deleteNode(root->rchild, next->data);
}
}
else if (root->data > x)
deleteNode(root->lchild, x);
else
deleteNode(root->rchild, x);
}
- 性质:其中序序列是有序的
4.8 平衡二叉树AVL
- 定义:仍然是二叉查找树,其左子树与右子树的高度之差的绝对值不能超过1(平衡因子)
struct node
{
int v, height;//v为结点权值,height为当前子树的高度
node* lchild, * rchild;
};
- 新建结点
node* newNode(int v)
{
node* Node = new node;
Node->v = v;
Node->lchild = Node->rchild = NULL;
Node->height = 1;//结点高度初始为1
}
- 获得当前结点的高度
int getHeight(node* root)
{
if (root == NULL) return 0;
return root->height;
}
- 计算平衡因子
int getBalanceFactor(node* root)
{
return getHeight(root->lchild) - getHeight(root->rchild);
}
- 更新高度
void updateHeight(node* root)
{
root->height = max(getHeight(root->lchild),
getHeight(root->rchild)) + 1;
}
- 查找操作:与二叉查找树相同
- 左旋(RR树型时使用)都要记得更新高度root和temp的高度
void L(node*& root)
{
node* temp = root->rchild;
root->rchild = temp->lchild;
temp->lchild = root;
updateHeight(root);
updateHeight(temp);
root = temp;
}
- 右旋(适用LL树型)
void R(node*& root)
{
node* temp = root->lchild;
root->lchild = temp->rchild;
temp->rchild = root;
updateHeight(root);
updateHeight(temp);
root = temp;
}
注:主要把最靠近插入结点的失衡结点调整到正常,路径上的所有结点就都会平衡
- 左旋右旋(适用于LR树型)
- 左旋右旋(适用RL树型)
- AVL插入情况汇总
树型 | 判定条件(BF为平衡因子) | 调整方法 |
---|---|---|
LL | BF(root)=2, BF(root->lchild)=1 | 对root进行右旋 |
LR | BF(root)=2, BF(root->lchild)=-1 | 先对root->lchild进行左旋,再对root进行右旋 |
RR | BF(root)=-2, BF(root->rchild)=-1 | 对root进行左旋 |
RL | BF(root)=-2, BF(root->rchild)=1 | 先对root->rchild进行右旋,再对root进行左旋 |
- 考虑平衡因子的插入结点操作
void insert(node* root, int v)
{
if (root == NULL)
{
root = newNode(v);
return;
}
if (v < root->v)
{
insert(root->lchild, v);
updateHeight(root);
if (getBalanceFactor(root) == 2)
{
if (getBalanceFactor(root->lchild) == 1)
{
R(root);
}
else if (getBalanceFactor(root->lchild) == -1)
{
L(root->lchild);
R(root);
}
}
}
else
{
insert(root->rchild, v);
updateHeight(root);
if (getBalanceFactor(root) == -2)
{
if (getBalanceFactor(root->rchild) == -1)
L(root);
else if (getBalanceFactor(root->rchild) == 1)
{
R(root->rchild);
L(root);
}
}
}
}
4.9 并查集:维护集合的数据结构:合并、查找、集合
- 定义:
int father[N];
其中father[N]
表示元素i的父亲结点,而father[i] = i
表示为根结点
例如:
father[1] = 1 //父亲结点是自己本身,则为根结点
father[2] = 1 //2的父亲结点是1
- 并查集的初始化
void initialize(int* father, int N)
{
for (int i = 1; i <= N; i++)
{
father[i] = i;
}
}
- 并查集的查找:寻找其根结点
int findFather(int father[], int x)
{
while (x != father[x])
{
x = father[x];
}
return x;
}
- 集合的合并:将两个不用根结点的结点合并,即合并其根结点
void Union(int *father, int f1, int f2)
{
int f1f = findFather(father, f1);
int f2f = findFather(father, f2);
if (f1f != f2f) father[f1f] = f2f;
}
- 并查集产生的每一个集合都是一棵树,不会产生环
- 路径压缩:把当前查询结点路径上的所有结点的父亲都指向根结点
4.10 大顶堆
大顶堆:每个结点的值都不小于其左右孩子结点的值,用于实现优先队列
- 当前结点向下调整
void downAdjust(int low, int high)
{
int i = low, j = i * 2;//i为要调整结点,j为i的左孩子结点
while (j <= high)
{
//左右孩子中最大的
if (j + 1 <= high && heap[j + 1] > heap[j])
{
j = j + 1;
}
//交换位置
if (heap[i] < heap[j])
{
swap(heap[i], heap[j]);
i = j;
j = 2 * i;
}
else
break;
}
}
- 建堆:从第一个非叶子结点向上调整
void createHeap()
{
for (int i = n/2; i >=1; i--)
{
downAdjust(i, n);
}
}
- 删除堆顶元素:只需要将最后一个元素覆盖堆顶元素,然后对其向下调整
void deleteTop()
{
heap[1] = heap[n--];
downAdjust(1, n);
}
- 添加一个元素:将新元素放在数组的最后,然后对其向上调整
- 向上调整程序
void upAdjust(int low, int high)
{
int i = high, j = i / 2;//i为调整结点,j为其父结点
while (j >= low)
{
if (heap[i] > heap[j])
{
swap(heap[i], heap[j]);
i = j;
j = i / 2;
}
else
break;
}
}
+ 添加元素程序
void insert(int x)
{
heap[++n] = x;
upAdjust(1, n);
}
4.11 堆排序(递增)
思路:每次将最后一个结点与堆顶结点交换,交换完后用层序遍历输出,得到递增序列
void heapSort()
{
for (int i = n; i >1; i--)
{
swap(heap[i], heap[1]);
downAdjust(1, i - 1);
}
}
4.12 哈夫曼树
- 概念
- 带权路径长度:叶子结点的权值乘以其路径长度的结果
- 树的带权路径长度WPL:所有叶子结点的带权路径长度之和
- 哈夫曼树:带权路径长度最小的树,也称最优二叉树
- 哈夫曼树不一定是唯一的,但是最小带权路径长度是唯一的
- 构建一个哈夫曼树思路
合并其中权值最小的两个结点,生成其父结点,权值为这两个结点之和,把父结点放回去,再重复抽两个最小结点,直到最后一个结点。 - 计算最小带权路径长度WPL
思路:每次都将选择出的两个结点权值一直累加并将总值入队,直到优先队列中的最后一个结点的权值不加。
int main()
{
int n, temp;
int x, y, wpl = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
scanf("%d", &temp);
q.push(temp);
}
while (q.size() > 1)
{
x = q.top();
q.pop();
y = q.top();
q.pop();
q.push(x + y);
wpl += (x + y);
}
printf("wpl: %d", wpl);
}
5 数据结构:图
5.1 邻接矩阵G[][]
G[i][j] = 1;
表示顶点i和j之间有边(值可为权重);只适用于顶点数目不大的题目
5.2 邻接表
对于一个图来说,每个顶点都可能有若干条出边,如果把同一个顶点的出边放在一个列表中,那么N个顶点就有N个列表,这个N个列表被称为图G的邻接表
- 如果邻接表只存放每条边的终点编号,邻接表可定义为
vector<int> Adj[N];
添加一条从1号顶点到3号顶点的有向边:Adj[1].push_back(3);
- 如果需要同时存放终点编号和边权,则通过结构体定义邻接表:
struct Node{
int v; //边的终点编号
int w;//边权
Node(int _v, int w_) : v(_v), w(_w) {} //构造函数
}
vector<Node> Adj[N];
添加一条从1号顶点到3号顶点权值为2的有向边Adj[1].push_back(Node(3,2));
5.3 图的遍历:深度优先搜索DFS
- 概念
- 思想:沿着一条路径直到无法继续前进,才退回到路径上离当前结点最近的还存在未访问分支顶点的岔路口,并前进访问那些未访问分支顶点,直到遍历整个图
- 连通分量:在无向图中,任意两个顶点可以互相到达,则图G为连通图,其中最大连通子图为连通分量。
- 强连通分量:在有向图中,任意两个顶点可以互相到达,则图G为强连通图,其中极大强连通子图为强连通分量。
- DFS伪代码
//访问顶点u
DFS(u){
vis[u] = true; //设置结点u已被访问
for( 从u出发能到达的所有结点v)
if( vis[v] == false)
DFS(v);
}
//遍历图G
DFSTrave(G){
for( 图G的所有结点u)
DFS(u);
}
5.4 图的遍历:广度优先遍历BFS
- 概念
+思想:建立一个队列,并把初始顶点加入到队列中,此后每次都取出队列的首顶点进行访问,并把该顶点可以到达的未曾加入队列的顶点全部加入到队列中,直到队列为空。 - BFS伪代码
BFS(u){
queue q;//定义队列q
将u入队
inq[u] = true;//设置u已被加入过队列
while(q非空){
取出q的队首元素u进行访问
for( 从u出发到达的所有结点v){
if( inq[v] == false){
将v入队
inq[v] = true;
}
}
}
}
BFSTravel(G){
for(G的所有结点)
if(inq[u] == false)
BFS(u);
}
5.5 单源最短路径:Dijkstra算法
- 概念:解决“单源最短路”问题:给定图G和起点s,通过算法得到S到达其他每个顶点的最短距离。并且要求边的权值为非负数。
- Dijkstra 算法的策略
设置集合S存放已被访问的顶点(即已访问的顶点),然后执行n次下面的两个步骤(n为顶点个数)- 每次从集合V-S(即未被访问的顶点)中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S(即已被访问)
- 之后,令顶点u为中介点,优化起点s与所有从u能到达的顶点v之间的最短路径
- Dijkstra 算法的具体实现:
- 集合S用一个bool型数组vis[]实现
- int型的d[]表示起点到达顶点v的距离,初始时除了给起点s的d[s]赋值为0,其他顶点可以赋值一个很大的数(1000000000,10的9次方)来表示inf
- int型数组pre[]记录当前结点的前驱结点
- Dijkstra 算法的伪代码
Dijstra(G, d[], s){
初始化;
for(循环n次){
u=使d[u]最小的还未被访问的顶点的标号;
记u已被访问;
for(u出发能够达到的所有顶点v)
if(v未被访问过 && 以u为中介点使s到达v的最短距离d[v]更优){
优化d[v];
记录前驱结点pre[];
}
}
}
- 邻居矩阵版代码
void Dijkstra(int s)//s为起点
{
//初始化
fill(d, d + maxn, inf);
d[s] = 0;
for (int i = 0; i < maxn; i++)
{
int u = -1, min = inf;//min = d[u]
for (int j = 0; j < maxn; j++)
{
if (vis[j] == false && min > d[i])
{
min = d[i];
u = j;
}
}
//找不到小于INF的d[u],表示剩下的顶点和起点s不连通
if (u == -1) return;
//以下部分区别于邻接矩阵和邻接表
for (int v = 0; v < maxn; v++)
{
if (vis[v] == false && G[u][v] != inf && d[u] + G[u][v] < d[v])
{
d[v] = d[u] + G[u][v];
pre[v] = u;
}
}
}
}
- 邻接表版代码 修改以上注释说明的部分
for (int j = 0; j < Adj[u].size(); j++)
{
int v = Adj[u][j].v;
if (vis[v] == false && d[u] + Adj[u][j].dis < d[v])
{
d[v] = d[u] + Adj[u][j].dis;
pre[v] = u;
}
}
- 输出最短路径
void DFS(int s, int v)//s为源点,v为当前访问的结点
{
if (v == s) {
printf("%d\n", v);
return;
}
DFS(s, prev[v]);
printf("%d\n", v);
}
5.6 单源最短路径:Floyd算法
- 定义:用来解决全源最短路问题,即给定图G,求任意两点u,v之间的最短路径长度,时间复杂度n的三次方,图适合使用邻接矩阵。
- Floyd算法的思路:
如果存在顶点k,使得以k作为中介点时顶点i和顶点j的当前最短距离缩短,则使用顶点k作为顶点i和顶点j的中介点。 - Floyd算法流程
枚举顶点k
以顶点k作为中介点,枚举所有顶点对i和j
如果dis[j][k] + dis[k][i] < dis[i][j]成立
赋值dis[i][j] = dis[j][k] + dis[k][i];
- Floyd算法实现
void Floyd()
{
for (int k = 0; k < n; k++)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (dis[i][k] != inf && dis[k][j] != inf &&
dis[i][k] + dis[k][j] < dis[i][j])
{
dis[i][j] = dis[i][k] + dis[k][j];
}
}
}
}
}
5.7 最小生成树
- 概念:是一个给定的无向图中求一棵树T,使得这棵树拥有图G中的所有顶点,且所有边都是来自图G的边,并且满足整棵树的边权之和最小。
- 性质:
- 最小生成树是树,因此其边数等于顶点数减一,且树内一定不会有环
- 对给定的图,其最小生成树不唯一,但其边权之和一定是唯一的
- 由于最小生成树是在无向图上生成的,因此根结点可以是这棵树上的任意一个结点,按照实际题目要求来
- 求解最小生成树有两种算法:prim算法,kruskal算法
5.8 最小生成树:prim算法
- prim算法
- 使用vis[]表示顶点是否已被访问
- int型数组d[]来存放结点v与集合S的最短距离,初始时除了起点S的d[s]赋值为0,其他都为inf
- ==prim算法与Dijkstra算法使用的思想几乎完全相同,只是数组d[]的含义上有所区别。==其中Dijkstra算法的数组d[]含义为起点s到达顶点v的最短距离,而prim算法的数组d[]含义为顶点v与集合S的最短距离
- prim算法伪代码
prim(G, d[]){
初始化;
for(循环n次){
u=使d[u]最小的还未被访问的顶点的标号;
记u已被访问;
for(从u出发能到达的所有顶点v)
if(v未被访问 && 以u为中介点使得v和集合S的最短距离d[v]更优)
将G[u][v]赋值给v与集合S的最短距离d[v];
}
}
- 邻接矩阵版代码(参考Dijkstra算法,只是d[]的含义不同)
const int maxn = 1000;
const int inf = 1000000000;
bool vis[maxn] = { false };
int d[maxn];
int G[maxn][maxn];
int n;
int prim()
{
int ans = 0;
fill(d, d + maxn, inf);
d[0] = 0;
for (int i = 0; i < n; i++)
{
int u = -1, MIN = inf;
for (int j = 0; j < n; j++)
{
if (vis[j] == false && d[j] < MIN)
{
u = j;
MIN = d[j];
}
}
//若没有找到任何一个不为inf的d[u],则剩下的顶点与集合S不连通
if (u == -1) return -1;
vis[u] = true;
ans += d[u];//将与集合s距离最小的边加入最小生成树
//以下区别邻接矩阵与邻接表
for (int v = 0; v < n; v++)
{
if (vis[v] == false && G[u][v] != inf && G[u][v] < d[v])
{
d[v] = G[u][v];
}
}
}
return ans;
}
- 邻接表版本代码修改以上注释说明的地方
for (int j = 0; j < Adj[u].size(); j++)
{
int v = Adj[u][j].v;
if (vis[v] == false && Adj[u][j].dis < d[v])
{
d[v] = Adj[u][j].dis;
}
}
5.9 最小生成树:kruskal算法
- kruskal算法思路
- 对所有边按照边权从小到大进行排序
- 按边权从小到大测试所有的边,如果当前测试边所连接的两个顶点不在同一个连通块,则把这条测试边加入当前最小生成树,否则,将边舍弃
- 执行步骤二,直到最小生成树中的边数等于总顶点数减一或是测试完所有的边时结束,而当结束时如果最小生成树的边数小于总顶点数减一,则说明该图不连通
- kruskal算法定义的结构:
需要定义一个结构体来定义边(包括边的端点和边权)
struct edge
{
int u, v;//表示边的两个顶点
int cost;//边权
} E[MAXE];//最多有MAXE条边
还需要对边进行排序,因此顶一个sort的比较函数
bool cmp(edge a, edge b)
{
return a.cost < b.cost;
}
判断两个端点是否是同一个连通块,则用到集合,通过并查集来实现:int father[N];
- kruskal算法的伪代码
int Kruskal(){
令最小生成树之和为ans、最小生成树当前边数为Num_Edge;
将所有边按边权从小到大排序;
for(从小到大枚举所有边){
if(当前测试边的两个端点在不同的连通块时){
将该测试边加入到最小生成树中;
ans += 测试边的边权;
最小生成树的当前边数Num_Edge +1;
当边数Num_Edge等于顶点数减一时结束循环;
}
}
return ans;
}
- 实现代码
int father[N];
int findFather(int x)
{
...
}
int kruskal(int n, int m)//n为顶点数,m为边数
{
int ans = 0, Num_Edge = 0;
for (int i = 0; i < n; i++)
{
father[i] = i;
}
sort(E, E + m, cmp);
for (int i = 0; i < m; i++)
{
int faU = findFather(E[i].u);
int faV = findFather(E[i].v);
if (faU != faV)
{
father[faU] = faV;
ans += E[i].cost;
Num_Edge++;
if (Num_Edge == n - 1) break;
}
}
return ans;
}
5.10 拓扑排序
- 概念:拓扑排序是将有向无环图G的所有顶点排成一个线性序列,使得对图G中的任意两个端点u, v,如果存在边u->v,那么在序列中u一定在v前面。
- 应用:判断一个给定的图是否是有向无环图
- 思路
- 定义一个队列Q,并把所有入度为0的结点加入队列
- 取队首结点,输出,并删去所有从它出发的边,并令这些边到达的顶点的入度减一,如果某个顶点的入度减为0,则将其加入队列
- 反复进行步骤二,直到队列为空,如果队列为空时入过队的结点数恰好为N,说明拓扑排序成功,图G为有向无环图,否则,拓扑排序失败,图G中有环
- 邻接表版代码
bool topologicalSort()
{
int num = 0;//记录加入拓扑序列的顶点数
queue<int> q;
for (int i = 0; i < n; i++)
{
if (inDegree[i] == 0)
q.push(i);
}
while (!q.empty())
{
int u = q.front();
q.pop();
for (int i = 0; i < G[u].size(); i++)
{
int v = G[u][i];
inDegree[v]--;
if (inDegree[v] == 0)
q.push(v);
}
G[u].clear();
num++;
}
if (num == n) return true;
else return false;
}
5.11 关键路径
- 概念:
- 顶点活动网AOV:用顶点表示活动,边集表示活动间的优先关系的有向图(可以转换成AOE图)
- 边活动图AOE:用带权的边集表示活动,用顶点表示事件的有向图,其中边权表示完成活动需要的时间;其表示的是一个工程的进行过程;都是有向无环图;
- AOE网主要解决两个问题:
- 工程起始到终点至少需要多少时间
- 哪条(些)路径上的活动被称为关键路径,关键路径上的活动称为关键活动,其中AOE网中最长路径被称为关键路径。
- 思路整理
- 设置数组e和l,其中
e[r]
和l[r]
分别表示活动ar的最早开始时间和最迟开始时间(如不必须保留,可以不设置数组,最后可以直接通过公式推断) - 求出以上两个数组后,就可以通过判断
e[r] == l[r]
是否成立来确定活动r是否是关键活动 - 设置数组ve和vl,其中
ve[i]
和vl[i]
分别表示事件i的最早发生时间和最迟发生时间 - 对于活动ar来说,只要在事件Vi最早发生时马上开始,就可以使得活动ar的开始时间最早,因此:
e[r] = ve[i]
- 如果
l[r]
时活动的最迟发生时间,那么l[r] + length[r]
就是事件Vj的最迟发生时间(length[r]
表示活动ar的边权),因此:l[r] = vl[j] - length[r];
通过以上分析,可以将求解数组e和l,转变成求解数组ve和vl;
- 设置数组e和l,其中
- 求解过程
- 有k个事件
Vi1 ~ Vik
通过相应的活动ar1 ~ ark
到达事件Vj,活动的边权为length[r1] ~ length[rk]
。假设已经算好了事件Vil ~ Vik
的最早发生时间ve[i1]~ve[ik]
,那么事件Vj的最早发生时间为max(ve[i1] + length[r1], …, ve[ik] + length[rk])
通过拓扑排序来求解
- 有k个事件
stack<int> topOrder;//用栈来记录拓扑排序
bool topologicalSort()
{
queue<int> q;
for (int i = 0; i < n; i++)
{
if (inDegree[i] == 0)
q.push(i);
}
while (!q.empty())
{
int u = q.front();
q.pop();
topOrder.push(u); //将u加入拓扑序列
for (int i = 0; i < G[u].size(); i++)
{
int v = G[u][i].v;
inDegree[v]--;
if (inDegree[v] == 0)
q.push(v);
//用ve[u]来更新u的后继结点v
if (ve[u] + G[u][i].w < ve[v])
{
ve[v] = ve[u] + G[u][i].w;
}
}
}
if (topOrder.size() == n) return true;
else return false;
}
从事件Vi出发通过相应的活动ar1 ~ ark
可以到达k个事件Vj1 ~ Vjk
活动边权为length[r1] ~ length[rk]
。假设已经算好了事件Vj1 ~ Vjk
的最迟发生时间vl[j1]~vl[jk]
,那么事件Vi的最迟发生时间为:min( vl[j1]-length[r1],…, vl[jk] – length[rk])
通过步骤a计算出的拓扑序列栈,来实现从后向前
//直接使用topOrder出栈即为逆拓扑序列,求解vl数组
while (!topOrder.empty())
{
int u = topOrder.top();
topOrder.pop();
for (int i = 0; i < G[u].size(); i++)
{
int v = G[u][i].v; //u的第i个后继结点v
if (vl[v] - G[u][i].w < vl[u])
vl[u] = vl[v] - G[u][i].w;
}
}
- 总代码
//关键路径,不是有向无环图则返回-1,否则返回关键路径长度
int CriticalPath()
{
memset(ve, 0, sizeof(ve));//将ve数组初始化为0;
if (topologicalSort() == false)//求解ve数组
return -1;//不是有向无环图
//不知道汇点则,取ve数组的最大值来初始化
fill(vl, vl + n, ve[n - 1]);//v1数组初始化为汇点的ve值
//直接使用topOrder出栈即为逆拓扑序列,求解vl数组
while (!topOrder.empty())
{
int u = topOrder.top();
topOrder.pop();
for (int i = 0; i < G[u].size(); i++)
{
int v = G[u][i].v; //u的第i个后继结点v
if (vl[v] - G[u][i].w < vl[u])
vl[u] = vl[v] - G[u][i].w;
}
}
//遍历邻接表的所有边,计算活动最早开始时间e和最迟开始时间l
for (int u = 0; u < n; u++)
{
for (int j = 0; j < G[u].size(); j++)
{
int v = G[u][j].v, w = G[u][j].w;
int e = ve[u], l = vl[v] - w;
if (e == l)
printf("%d->%d\n", u, v);//输出关键活动
}
}
return ve[n - 1];//返回关键路径长度
}
6 字符串匹配:KMP算法
6.1 next数组求解
next[i]
:- 表示使子串
s[0...i]
的前缀s[0...k]
等于后缀s[i-k, k]
的最大k - k为所求最长相等前后缀中前缀的最后一位的下标,找不到相等前后缀,则令
next[i] = -1;
- 每次求出
next[i]
时,总让j指向next[i]
;
- 表示使子串
- next数组求解过程
- 初始化next数组,令
j = next[0] = -1;
- 让
i
在1 ~ len-1
范围遍历,对每个i
,执行步骤三四,以求解next[i]
- 不断令
j = next[j]
,直到j
回退到-1
,或是s[i] == s[j + 1]
成立 - 如果
s[i] == s[j + 1]
成立, 则令next[i] = j+1;
否则令next[i] = j;
- 初始化next数组,令
- 程序:
void getNext(char s[], int len)
{
int j = -1;
next[0] = -1;
for (int i = 1; i < len; i++)
{
if (j != -1 && s[i] != s[j + 1])
{
j = next[j];
}
if (s[i] == s[j + 1])
j++;
next[i] = j;
}
}
6.2 KMP算法
next[]
数组的作用:就是当j+1
失配时,j应该回退到的位置- KMP算法思路
- 初始化
j = -1;
,表示pattern(匹配串)当前已被匹配的的最后位 - 让
i
遍历文本串text,对每个i
,执行步骤三四来试图匹配text[i]
和pattern[j+1]
- 不断令
j = next[j];
,直到j = -1
,或是text[i] == pattern[j+1]
成立 - 如果
text[i] == pattern[j+1]
成立,则令j++
,如果j
达到m-1
(m为匹配串长度),则说明pattern是text的子串,返回true
- 初始化
- 代码
bool KMP(char text[], char pattern[])
{
int n = strlen(text), m = strlen(pattern);
getNext(text, n);
int j = -1;
for (int i = 0; i < n; i++)
{
while (j != -1 && text[i] != pattern[j + 1])
{
j = next[j];
}
if (text[i] == pattern[j + 1])
j++;
if (j == m - 1)
return true;
}
return false;
}
7 排序算法
7.1 直接插入排序(插入排序)
- 思路:
对序列A的n个元素A[1]~A[n]
,令i从2到n枚举,进行n-1趟操作。假设某一趟时,序列A的前i-1
个元素A[1]~A[i-1]
已经有序,而范围A[i]~A[n]
无序,那么该趟从范围[1, i-1]
中寻找某个位置j
,使得A[i]
插入位置j
后,范围[1, i]
有序。 - 代码
void insertSort(int *A, int n)
{
//直接插入排序:从小到大
for (int i = 2; i <= n; i++)//n-1趟
{
int temp = A[i];
int j = i;
while (j > 1 && A[j - 1] > temp)
{
A[j] = A[j - 1];
j++;
}
A[j] = temp;
}
}
7.2 希尔排序(插入排序)
- 思路:
将一个数组A[]
,将其中等间隔的元素组成一组(元素下标相差gap
),并对组内元素进行插入排序,之后再将gap
减小,再组内排序,直至gap
为1
,最后一次排序。 - 代码
void insertSort(int A[], int gap, int i)
{
int x = A[i];
int j;
for (j = i - gap; j >= 0 && A[j] > x; j-=gap)
{
A[j + gap] = A[j];
}
A[j + gap] = x;
}
void shellSort(int A[], int n)
{
for (int gap = n/2; gap >= 1; gap/=2)
{
for (int i = gap; i < n; i++)
{
insertSort(A, gap, i);
}
}
}
7.3 冒泡排序(交换排序)
- 思路:
(效率最低)每次通过交换的方式把当前剩余元素的最大值移动到一端,当剩余元素减少到0时,排序结束 - 代码
void bubbleSort(int a[], int N)
{
for (int i = 1; i < N; i++) //n-1趟
{
bool changed = false;
for (int j = 0; j < N - i; j++)
{
if (a[j] > a[j + 1])
{
swap(a[j], a[j + 1]);
changed = true;
}
}
if (changed == false)
{
break;
}
}
}
7.4 快速排序(交换排序,分治思维)
- 思路
利用分治的思维,给定一个待排序数组A[left, right]
,将哨兵x=A[left]
,设一个标记i
指向A[left]
,另一个标记j
指向A[right]
。首先将j
向前移动,找到第一个比x
小的元素,将其覆盖A[i]
的值,然后将i
向后移动,找到第一个比x
大的元素,将其覆盖A[j]
的值,反复以上步骤,直到i==j
为止,则i
的位置就是x
的正确排序位置,这时,i
下标的左边全部元素都小于x
,i
下标右边全部元素都大于x
,分别再递归求解。 - 代码
int partition(int A[], int left, int right)
{
int x = A[left];
int i = left, j = right;
while (i != j)
{
while (i<j && A[j]>= x)
j--;
if (i < j)
A[i] = A[j];
while (i < j && A[i] <= x)
i++;
if(i <j)
A[j] = A[i];
}
A[i] = x;
return i;
}
void quickSort(int A[], int left, int right)
{
if (left < right)
{
int mid = partition(A, left, right);
quickSort(A, left, mid - 1);
quickSort(A, mid + 1, right);
}
}
7.5 堆排序(选择排序)
堆排序相关知识请看4.11 堆排序(递增)
7.6 归并排序(分治思维)
- 思路:
利用分治的思维,将n
个元素的序列分解成n/2
个元素的两个子序列,并递归分解,当待排序的序列长度为1
时,递归开始回升。利用一个函数MERGE(A,p,q,r)
来完成合并,其中A
为一个数组,p,q
和r
是数组下标,满足p<=q<r
,该过程假设子数组A[p,q]
和A[p+1,r]
都已经排好序,它合并两个子数组形成单一的已经排好的数组来代替A[p,r]
。 - 代码
void merge(int A[], int left, int mid, int right)
{
//从小到大排序
//两个待合并的数组A[left. mid]和A[left+1,right]
int nl = mid - left + 1;
int nr = right - mid;
vector<int> L;
vector<int> R;
for (int i = left; i <= mid; i++)
{
L.push_back(A[i]);
}
for (int j = mid+1; j <= right; j++)
{
R.push_back(A[j]);
}
int i = 0;
int j = 0;
int k = left;
while (i < L.size() && j < R.size())
{
if (L[i] < R[j])
A[k++] = L[i++];
else
A[k++] = R[j++];
}
while (i < L.size()) A[k++] = L[i++];
while (j < R.size()) A[k++] = R[j++];
}
void mergeSort(int A[], int left, int right)
{
if (left < right)
{
int mid = (left + right) / 2;
mergeSort(A, left, mid);
mergeSort(A, mid + 1, right);
merge(A, left, mid, right);
}
}
7.7 基数排序
- 代码
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int maxData = data[0]; ///< 最大数
/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
for (int i = 1; i < n; ++i)
{
if (maxData < data[i])
maxData = data[i];
}
int d = 1;
int p = 10;
while (maxData >= p)
{
//p *= 10; // Maybe overflow
maxData /= 10;
++d;
}
return d;
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}