数据结构 数组、串与广义表

数据结构 数组、串与广义表

多维数组的存储

多维数组在内存中是以一维形式储存的。对于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)。

广义表

广义表是元素可以为表的推广线性表。

广义表的元素可以是原子元素,也可以是一个表。广义表的表头是广义表的第一个元素,广义表的表尾是由除表头以外的所有元素组成的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值