数据结构 数组、串与广义表
多维数组的存储
多维数组在内存中是以一维形式储存的。对于n维数组,有以下映射关系:
数组a[m1][m2]…[mn]中,元素a[i1][i2]…[in]的内存位置为:
a + i1*m2*m3*…*mn + i2*m3*m4*…mn + … + in
特殊矩阵的存储
对称矩阵的压缩存储
对称矩阵以上三角矩阵为例,压缩成一维数组存储,可以节省一半空间,其映射关系如下:
上三角矩阵a[m1][m2],元素a[i][j]的对应一维矩阵的元素内存位置为:
a + n + n-1 + … + i-1 +j
因此对称矩阵假如i>j,对调二者位置即可。
稀疏矩阵的压缩存储
稀疏矩阵可以以一个三元组的形式储存在一维数组中。三元组为:<行号,列号,元素>,按照行号-列号递增排序的方式储存在一维数组中。
#include <iostream>
using namespace std;
struct node{
int row;
int col;
int val;
};
int main(){
node A[10];
// 书上的矩阵
A[0].row = 0;A[0].col = 1;A[0].val = 3;
A[1].row = 1;A[1].col = 4;A[1].val = 9;
A[2].row = 2;A[2].col = 0;A[2].val = 2;
A[3].row = 3;A[3].col = 1;A[3].val = -11;
A[4].row = 3;A[4].col = 2;A[4].val = -6;
A[5].row = 3;A[5].col = 5;A[5].val = -8;
A[6].row = 4;A[6].col = 4;A[6].val = 19;
A[7].row = 5;A[7].col = 3;A[7].val = -17;
A[8].row = 6;A[6].col = 5;A[6].val = -52;
}
稀疏矩阵的转置
我们知道,稀疏矩阵在存储时,需要按照行号-列号作为排序标准,因此在转置后,就需要按照列号-行号作为排序标准。假如不在转置后排序的话,那就需要按照列号依次在数组中遍历col次,总复杂度达到O(col*col*row)
为了减小复杂度,可以设置两个辅助数组,rowSize[]和rowStart[],其中rowSize[]统计转置后的矩阵每一行的非零元素数量,也就是转置前的矩阵每一列的非零元素数量。根据rowSize[],就可以确定转置后矩阵每一行开始的储存位置rowStart[]。接着遍历原矩阵,在对应转置后的行位置添加转置后的三元组,然后本行rowStart{}++即可。总复杂度来到了O(col + col*term)
#include <iostream>
using namespace std;
struct node{
int row;
int col;
int val;
};
int cols = 6;
int rows = 7;
void T(node A[],node B[],int n){
int rowSize[cols];// 记录转置后每一行的长度
int rowStart[cols];// 记录转置后每一行的起始位置
memset(rowSize, 0, sizeof(rowSize));
memset(rowStart, 0, sizeof(rowStart));
// 遍历统计各列数量
for(int i = 0;i < n;i++){
rowSize[A[i].col]++;
}
// 根据各列数量确定起始位置
for(int i = 1;i < cols;i++){
rowStart[i] = rowStart[i-1] + rowSize[i-1];
}
// 根据转置后各行的起始位置确定本元素应放在何位置
for(int i = 0;i < n;i++){
int j = rowStart[A[i].col];
B[j].col = A[i].row;
B[j].row = A[i].col;
B[j].val = A[i].val;
rowStart[A[i].col]++;
}
}
int main(){
node A[10];
int n = 9;
// 书上的矩阵
A[0].row = 0;A[0].col = 1;A[0].val = 3;
A[1].row = 1;A[1].col = 4;A[1].val = 9;
A[2].row = 2;A[2].col = 0;A[2].val = 2;
A[3].row = 3;A[3].col = 1;A[3].val = -11;
A[4].row = 3;A[4].col = 2;A[4].val = -6;
A[5].row = 3;A[5].col = 5;A[5].val = -8;
A[6].row = 4;A[6].col = 4;A[6].val = 19;
A[7].row = 5;A[7].col = 3;A[7].val = -17;
A[8].row = 6;A[8].col = 5;A[8].val = -52;
node B[10];
T(A,B,n);
int res[cols][rows];
memset(res, 0, sizeof(res));
for(int i = 0;i < n;i++) res[B[i].row][B[i].col] = B[i].val;
for(int i = 0;i < cols;i++){
for(int j = 0;j < rows;j++){
cout<<res[i][j]<<" ";
}
cout<<endl;
}
}
字符串
常用函数与用法
#include <cstdio>
#include <string.h>
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main(){
/** c语言字符串 */
// 字符串的定义
char s1[] = "abc";// 不指定长度
char s2[5] = "abc";// 指定长度,二者等效
char s3[] = {'a','b','c','\0'};// 本质上是带结束符的字符数组
char s4[5] = {'a','b','c'};// 初始化为0的元素对应的ASCII码刚好是\0
// 字符串的输入输出
printf("%s\n",s1);// printf输出格式控制为%s
printf("%s ! ! !\n",s2);// 可以自己进行输出格式的定义
puts(s3);// 也可以用puts输出,自带换行,但是无法进行格式定义
scanf("%s",s4);// 换行、tab和空格均会结束,但不会读入
gets(s4);// 仅有换行会结束,不会读入换行符,有可能溢出数组,不安全
fgets(s4,5,stdin);// 同样仅有换行会结束,但是这里提前指定了缓冲区的最大宽度,fgets既可以读文件,也可以读键盘,安全的用法
// 字符串的操作函数
strcat(s1, s3);// 将s3连接到s1上
strncat(s1, s3, 1);// 将s3的前n个字符连接到s1上
printf("%d\n",(int)strcspn(s1, "bc"));// 检查s1中第一次出现字符集bc中任一字符的位置,若不存在则返回字符串长度
strcpy(s1,s3);// 将s3复制到s1上
strncpy(s1, s3, 1);// 将s3的前n个字母复制到s1的前n个字符上
printf("%d\n",(int)strlen(s1));// 字符串长度
printf("%d\n",(int)strcmp(s1, s3));// 比较两字符串的字典序,正0负分别代表大于等于小于
// 字符串数组
char ss[2][5] = {"asdf","asdx"};
printf("%s %s\n",ss[0],ss[1]);
/** c++字符串 */
// 字符串的定义
string str1 = "cba";// 常量定义
string str2 = string("cba");// 构造函数定义
string str3 = s1;// 直接由c语言字符串赋值
string str4(5,'n');// 若干个重复字符定义
// 字符串的输入输出
cout<<str1<<endl;// 输出
printf("%s\n",str2.c_str());//printf输出需要转换为c语言字符串
cin>>str3;// 输入不包括空格换行和tab
// 字符串的操作函数
cout<<str1.size()<<endl;// 字符串长度
cout<<str2.substr(0,1)<<endl;// 获取子串,左闭右开
cout<<str1.append(str2)<<endl;// 将str2连接在str1上
cout<<str1.insert(1, "K")<<endl;// 将子串Ks插入到str1的第二个位置
cout<<to_string(1234)<<endl;// 字符串转整数,c++11
size_t pos = str1.find("bca");
if(pos != string::npos) cout<<pos<<endl;// 查找str1中子串bca第一次出现的位置
cout<<str1.compare(str2)<<endl;// 比较字典序,同strcmp
// 字符串数组
string strstr[3] = {"aa","cc","bb"};
sort(strstr, strstr+3);
cout<<strstr[0]<<" "<<strstr[1]<<" "<<strstr[2]<<endl;// 可以直接用sort对string按照字典序排序
}
BF算法
查找字符串T中子串P的位置,最简单的一种方法就是暴力,也就是BF,这是一种带回溯的方式,每次匹配失败后从下一个位置开始再次匹配,复杂度达到O(mn)。
KMP算法
进一步地,我们可以提前根据模式串P和目标串T的特征,先确定一种搜索规则,当一个开始位置不行的时候,直接跳过后面已经看出不行的开始位置,到下一个有可能成功的开始位置。
如何更好地理解和掌握 KMP 算法? - 海纳的回答 - 知乎 https://www.zhihu.com/question/21923021/answer/281346746
我们观察一个匹配失败的一般状态,T串的第i个元素和P串的第j个元素不匹配,那么我们就要考虑P串下一次从T串的哪一个位置开始匹配了。既然T串第i个元素和P串的第j个元素不匹配,那么T串中前j-1个元素和P串中的前j-1个元素一定是匹配的,那么下一个位置,就由这j-1个元素中第二个匹配位置决定。问题转化为P中前j-1个元素组成的子串的前缀集合和T中前I-1个元素的后缀集合中交集里长度最大的元素。
定义一个部分匹配表pmt,pmt[i]就是T串前j-1个元素组成的子串(也就是P串前j-1个元素组成的子串)前缀集合和后缀集合交集中最长元素的长度,那么刚才的例子中,第二个匹配位置就是i-pmt[j-1]。
剩下的问题就是如何将这个部分匹配表求出来。可以说这部分是kmp的精髓。当然可以直接m方求,复杂度已经低了不少了,但是实际上复杂度可以低至m。然而上面的博客这部分讲的很简略,因此这部分参考下面的博客:
如何更好地理解和掌握 KMP 算法? - 阮行止的回答 - 知乎 https://www.zhihu.com/question/21923021/answer/1032665486
我们稍微改变一下pmt的定义,用next来表示pmt数组,左闭右开代替左闭右闭,更加适合编程。next[i]是编号为0-i-1的元素构成的子串中最长公共前后缀长度。规定next[0] = -1。
求解过程是一个动态规划的思想。假设已经求得了next[i],那么在求解next[i+1]时,会有两种情况。第一种情况,P[i] = P[next[i]],这种情况下直接将这个新加入的字符加入最长公共前后缀即可,next[i+1] = next[i] + 1;第二种情况,二者不等,这时next[i+1]一定小于next[i],为什么?因为如果可以扩大原来的最长公共前后缀,那原来的就不足以称之为最长了。因此我们看P[next[next[i]]] 是否等于 P[i],如果相等,则next[i+1] = next[next[i]] + 1,否则继续按照相似的方式迭代。
至此,kmp就完成了。
#include <iostream>
using namespace std;
const int maxt = 1000;
const int maxp = 50;
char T[maxt],P[maxp];
int Next[maxp];
int m,n;
// 求出各个位置的next
void getNext(){
int i,j;// i是当前求解的位置,j是前后缀长度(now指针)
j = Next[0] = -1;
i = 0;
while(i < m){
// 直到新来的字符可以匹配为止
while(j != -1 && P[i] != P[j]){
j = Next[j];
}
Next[++i] = ++j;
}
}
// 进行匹配,返回匹配的位置数
int kmp(){
int i,j;// 分别是T串和P串的位置指针
int ans = 0;
// 预处理next数组
getNext();
i = j = 0;
while(i < n){
// 假如不匹配了,下次T串从i开始,P串从next[j]开始,直到该位置匹配,然后去匹配下一位置
// j = -1说明要重新开始匹配了,从下一个位置开始
while (j != -1 && T[i] != P[j]) {
j = Next[j];
}
// 假如匹配了,那就匹配下一个位置
i++;j++;
// 假如完全匹配成功了
if(j >= m){
ans++;
j = Next[j];// 别忘了更新
}
}
return ans;
}
int main(){
cin>>T>>P;
m = (int) strlen(P);
n = (int) strlen(T);
cout<<kmp()<<endl;
}
kmp的复杂度达到了O(m+n)。
广义表
广义表是元素可以为表的推广线性表。
广义表的元素可以是原子元素,也可以是一个表。广义表的表头是广义表的第一个元素,广义表的表尾是由除表头以外的所有元素组成的表。