【数据结构Note4】-串、数组和广义表(kmp算法详解)

news2024/10/7 12:18:05

串、数组和广义表

顺序表和链表分别是线性表的两种存储结构。

栈和队列是操作受限的线性表。

串、数组和广义表是内容受限的线性表。

1. 串

1.1 串的概念和结构

串(String)—零个或多个任意字符组成的有限序列

所谓串是内容受限的线性表。就是要求该线性表的元素类型只能是字符型,而一般的线性表元素任意。串的逻辑结构如下:

image-20221022224846014
  • 子串:一个串中任意个连续字符组成的子序列(含空串)称为该串的子串。
  • 真子串:是指不包含自身的所有子串。
  • 主串:包含子串的串称为主串
  • 字符位置:字符在序列中的序号为该字符在串中的位置
  • 子串位置:子串第一个字符在主串中的位置
  • 空格串:有一个或多个空格组成的串,与空串不同
  • 串相等:串长相等且对应位置字符相等(空串都相等)

串作为内容受限的线性表,同样有两种存储结构,串的顺序存储结构叫顺序串,串的链式存储结构叫链串。当然有顺序存储就会相应的有静态串和动态串

1.2 顺序串和链串

  1. 顺序串

    其实就是顺序表中数据元素改成char类型,按空间分配类型可分为静态顺序串和动态顺序串,下面是静态顺序串定义:

    //顺序串
    #define MAXSIZE 255
    struct SString {
    	char ch[MAXSIZE+1];//下标范围0~MAXSIZE
    	//0号下标元素默认不用,这样在一些算法中能带来简便
    	int length;//当前串的长度
    };
    

    相比链串,由于实际中很少队串进行插入删除操作,所以顺序串的使用更加广泛。对于串的特殊运算,后面会讲解。但顺序串的一些基本操作可参考这篇文章:

    [(243条消息) 【数据结构笔记】- 顺序表_Answer-2296的博客-CSDN博客_数据结构顺序表的总结](

    https://blog.csdn.net/weixin_63267854/article/details/124453493?spm=1001.2014.3001.5501)

  2. 链串

    结点数据于存储字符,指针域串联字符形成串。

    使用何种类型的链表,则根据具体情况,赋予链表不同性质:单向,双向,带头,不带头,循环,非循环。

    相比于顺序串,链串操作方便(优点),但存储密度较低(缺点,链式存储通病)

    image-20221022233209315

    如果一个结点数据域只存储一个字符,存储密度就是20%,很低!如下

    image-20221022233628084

    为了提高存储密度,一个结点数据域存储多个字符,提高存储密度的同时,保留链串操作的简便性

    image-20221022233821392

    相应的结点中存储字符的那一部分称为“块”,相应的我们将串的链式存储结构称为块链结构

    下面为块链结构的定义

    //链串
    #define CHUNKSIZE 80
    struct Chunk {
    	char ch[CHUNKSIZE];
    	Chunk* next;
    };
    struct LString {
    	Chunk* head, * tail;//串的头指针和尾指针
    	int curlen;//串当前长度
    };//块链结构
    

    对于串的特殊运算,后面会讲解。但链串的一些基本操作可参考这篇文章:

    (243条消息) 【数据结构笔记】- 链表 - 基础到实战-入门到应用_Answer-2296的博客-CSDN博客

1.3 BF算法–串的模式匹配法之一

串的模式匹配算法:确定主串中所含子串(模式串)第一次出现的位置(定位)。应用于搜索引擎、拼写检查等,常见的算法有BF算法和KMP算法

BF算法(Brute-Force),采用穷举法的思路。就是从主串S第一个字符一次与T的字符进行匹配。具体实现如下

//匹配成功,返回模式串p在文本串s中的位置,否则返回-1
int ViolentMatch(char* s, char* p)
{
	int sLen = strlen(s);
	int pLen = strlen(p);
 
	int i = 0;
	int j = 0;
	while (i < sLen && j < pLen)
	{
		if (s[i] == p[j])
		{
			//如果当前字符匹配成功(即S[i] == P[j]),则i++,j++    
			i++;
			j++;
		}
		else
		{
			//如果失配(即S[i]! = P[j]),i回溯即令i = i - (j - 1),j重新开始:j = 0    
			i = i - j + 1;//相当于刚才移动了j-0个字符,现在回溯就是i-j+0,但该位置比过了,所以比对i-j+1
			j = 0;
		}
	}
	//匹配成功,返回模式串p在文本串s中的位置,否则返回-1
	if (j == pLen)
		return i - j;
	else
		return -1;
}

BF算法简单,代码凝练

  • BF算法的时间复杂度

    假设主串长度为n,模式串的长度为m。则BF算法的最好时间复杂度为m,最坏时间复杂度为(n-m+1)*m

1.5 KMP算法–串的模式匹配法之一

以下算法按照C++中的字符串string讲解(字符数组从0开始计数,有些字符数组从1开始计数,算法差异后续讲解,但实质未变)

KMP算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。

KMP算法充分利用部分已经匹配的结果加快串的模式匹配。

KMP算法思路讲解:用下标i遍历主串且不回溯,比对的模式串下标j在失配是遵守next[j]数组回溯。(next[j]是一个数组,按照特定规则计算得到的模式串下标j回溯的位置)KMP算法时间复杂度为O(n+m)。

  • KMP算法概述

KMP算法的核心在于数组next[j],模式串中每一个字符都在next数组中有唯一对应值,该值表示主串和模式串字符不匹配时模式串下标j回溯的位置,具体next[j]如何的来和KMP算法为何可以这样回溯在后文有讲解,这里先接受有这样一个next数组。以下是KMP算法实现

  • kmp算法和BF算法区别就在于主串i不回溯,子串j按照next[j]来回溯(后面会介绍会什么可以这样回溯!)

KMP算法实现如下:

int KmpSearch(char* s, char* p)
{
	int i = 0;
	int j = 0;
	int sLen = strlen(s);
	int pLen = strlen(p);
	while (i < sLen && j < pLen)
	{
		//如果j = -1说明前一轮字符匹配失败主串后移,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++   
		if (j == -1 || s[i] == p[j])
		{
			i++;
			j++;
		}
		else
		{
			//如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]    
			//next[j]即为j所对应的next值      
			j = next[j];
		}
	}
	if (j == pLen)//说明匹配成功
		return i - j;//返回下标
	else
		return -1;
}

了解了KMP算法,下面讲解next数组

1.5.1 next数组

首先了解串中的两个概念,对于一个串P=P1P2P3…Pj-1Pj,对于下标j而言有

  • 前缀:串P中包含P1的子串,但不包括串P本身
  • 后缀:串P中包含Pj的子串,但不包括串P本身

next数组的作用

KMP的next 数组相当于告诉我们:当模式串中的某个字符跟文本串中的某个字符匹配失配时,模式串下一步应该跳到哪个位置。如模式串中在j 处的字符跟文本串在i 处的字符匹配失配时,下一步用next [j] 处的字符继续跟文本串i 处的字符匹配

按步求解next数组

  1. 寻找模式串前缀后缀的最长公共元素长度

    对于 P = P0P1 … Pj-1 Pj,寻找模式串P中长度最大且相等的前缀和后缀。如果存在 P0P1 …Pk-1Pk = Pj-kPj-k+1…Pj-1Pj,那么在包含 Pj 的模式串中有最大长度为k+1的相同前缀后缀。就是寻找寻找模式串前缀后缀的最长公共元素长度,这里以模式串“abab”为例子,得到一个数组:

    image-20221025142226085
  2. 将上一步数组整体右移一位,最右边截断,最左边补-1得到的数组就是next数组了(世界上next[j]就是P = P0P1 … Pj-1 的相等前后缀的最长长度,而对于第一个没有j-1字符,就设置为最小下标-1,其实这是个无用的值,在后续算法中只是作为一个标识而已)

    image-20221025142351661
  3. 根据next数组匹配

    当主串和模式串字符失配时,模式串下标j则向左移动j-next[j],对应下标也就是j=j-(j-next[j])=next[j]。所以当主串和模式串字符失配时,j=next[j]。

    image-20221025152055988

按next数组回溯产生的效果

主串下标i不用回溯,模式串下标j部分回退,提高效率。

下图主串和模式串匹配好的字符时灰色框:ABCDHUABCD,但图中可以看到下一次比对中H和E失配了,那么模式串j回溯到哪?

按next数组回溯到下面那副图中也就是H。这时主串和模式串中匹配好的字符并非从零开始,而是新的灰色框:ABCD,也印证那句话:KMP算法充分利用部分已经匹配的结果加快串的模式匹配。

image-20221025145034637

那么为什么next数组能产生如此效果?为什么KMP算法主串下标不用回退?

首先回答第二个问题,为什么主串下标可以不回退?担心这个问题的伙伴无非顾虑的是担心主串和模式串失配时,主串下标不回溯会不会遗漏和模式串匹配的字符?答案是不会!

因为有next数组!next数组求解的就是失配下标前的相等的前后缀最大长度,也就是失配后主串下标前能和模式串从头开始匹配多少个字符?上面的图中H和E失配后主串前面ABCD便能和模式串中的ABCD匹配,这样的工作已经在求解next数组的时候完成了,所以主串下标是无需回退的。

回到求解next数组那一步,核心时求解相等前缀后缀的最大长度!求解next[j]也就是求解模式串钱j-1个元素的相等前后缀的最大长度,这里设最大长度为length,直接next[j]=length。length对应的下标刚好就是刚才求解得到的最大前缀的下一个字符,所以主串和模式串失配时,j=next[j],下一步主串和模式串比对的字符前面的就是新的前缀和后缀了,在上图对应的新前后缀就是ABCD了,所以说KMP算法利用了已匹配字符序列,让i不回溯,j减少回溯。提高效率。

1.5.2 递推求next数组

这里先给出代码实现

void GetNext(char* p,int next[])
{
	int pLen = strlen(p);
	next[0] = -1;
	int k = -1;
	int j = 0;
	while (j < pLen - 1)//只需要遍历到倒数第二个元素
	{
		//p[k]表示前缀,p[j]表示后缀
		if (k == -1 || p[j] == p[k]) 
		{
			++k;
			++j;
			next[j] = k;
		}
		else 
		{
			k = next[k];
		}
	}
}

核心:根据next[i]前面的数值求解next[i]

这里用到的两个下标,j时上一层循环的最大前缀的下一个字符的下标,i表示上一层循环得最大后缀得下一个,也就是待求得next数组下标。不理解这句话最好的解决办法就是,写出循环每一次对ij修改的情况,并观察模式串对应位置。算法种初值设置有讲究,后面会讲。初值设定:next[0] = -1; int k = -1; int j = 0;设模式串为T。

i作为遍历模式串的下标,循环中比较p[i]是否等于p[j],会分三种情况

  1. 当p[i] == p[j],回看这句话:j时上一层循环的最大前缀的下一个字符的下标,i表示上一层循环得最大后缀的下一个,p[i] == p[j]说明上一个前后缀往后延一个,前后缀任然相等,就直接赋值了next[i] = j;

  2. 当p[i] != p[j],就令j=next[j];寻找长度更短的相同前缀后缀。

    为何递归前缀索引j=next[j],就能找到长度更短的相同前缀后缀呢?

    我们拿前缀 p0…pj-1 pj 去跟后缀pi-j…pi-1 pi匹配,如果pk 跟pj 失配,下一步就是用p[next[k]] 去跟pj 继续匹配,如果p[ next[k] ]跟pj还是不匹配,则需要寻找长度更短的相同前缀后缀,即下一步用p[ next[ next[k] ] ]去跟pj匹配。此过程相当于模式串的自我匹配,所以不断的递归k = next[k],直到要么找到长度更短的相同前缀后缀,要么没有长度更短的相同前缀后缀,也就是第三种情况了。

    image-20221025172923272

  3. j == -1,出现j == -1说明已经没有字符和p[i]比较了,也就是无法找到更短的前后缀了。直接令next[i]=0或者next[i]=++j。如此一来,主串和模式串失配时,模式串回溯到next[i]也就是0,就是从头开始比对,因为没有相等得前后缀了嘛。

    出现j == -1情况有两情况,一种时初值j == -1,另一种就是在循环中不断寻找更小得前后缀时(j=next[j]),所要找的前后缀不断缩小,直到没有,此时对应j == -1。

初值设定很讲究

next[0] = -1;
int j = -1;
int i = 0;

将next数组第一个元素和j均设置为next数组最小下标减1的值。i设置成next数组最小下标。为什么我这里强调是最小下标减1?因为一些串结构中的0号位置是不存储数据的!这样的串在求next数组时的初值就不能照搬了。因为串的下标的标准不一样,对应的next数组时不一样的。相同的字符对应的数组值回相差串最小下标之差。如对于串首元素下标为0和串下标首元素为1的next数组如下:

image-20221029232443319

但我们在求解next数组的时候,在初值上做了区别,具体实现上使用递归,规避了求解next数组的差异性,使得不同标准的串都能用这一套算法求解!

回过来,都知道-1在数组中是无效下标,那next[0]和j初始值为什么不能设置成-5?其他无效下标呢?答案是当然可以啦,只是在kmp算法实现时需做特殊处理。解释钱

先看next[0]和j初值为1的算法

int KmpSearch(char* s, char* p)
{
	int i = 0;
	int j = 0;
	int sLen = strlen(s);
	int pLen = strlen(p);
	while (i < sLen && j < pLen)
	{
		//如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++    
		if (j == -1 || s[i] == p[j])
		{
			i++;
			j++;
		}
		else
		{
			//如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]    
			//next[j]即为j所对应的next值      
			j = next[j];
		}
	}
	if (j == pLen)
		return i - j;
	else
		return -1;
}

值得注意当j == -1时,也就是下标无效时,kmp算法中让主串i,j均自增,使得j回归正常的模式串最小下标了,主串字符也后移动一个了。简单的加1回归正常值,就是初值设定的妙处。如果next[0]=-5,j=-5不仅是kmp算法,而且是求解next数组的代码在j==-5时都需要单独对j和i赋值,使得下标回归正常值。所以这样的初值设定使得代码最具整洁性。

某专家博主说,next数组有些像状态机,笔者认为很有道理:

next 负责把模式串向前移动,且当第j位不匹配的时候,用第next[j]位和主串匹配,就像打了张“表”。此外,next 也可以看作有限状态自动机的状态,在已经读了多少字符的情况下,失配后,前面读的若干个字符是有用的。

image-20221025181708003

以上便是KMP算法了,不得不敬佩那三位科学家,牛掰的

KMP算法完整实现

//求next数组
void GetNext(char* p,int next[])
{
	int pLen = strlen(p);
	next[0] = -1;
	int k = -1;
	int j = 0;
	while (j < pLen - 1)
	{
		//p[k]表示前缀,p[j]表示后缀
		if (k == -1 || p[j] == p[k]) 
		{
			++k;
			++j;
			next[j] = k;
		}
		else 
		{
			k = next[k];
		}
	}
}
//模式匹配
int KmpSearch(char* s, char* p)
{
	int i = 0;
	int j = 0;
	int sLen = strlen(s);
	int pLen = strlen(p);
	while (i < sLen && j < pLen)
	{
		//如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++    
		if (j == -1 || s[i] == p[j])
		{
			i++;
			j++;
		}
		else
		{
			//如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]    
			//next[j]即为j所对应的next值      
			j = next[j];
		}
	}
	if (j == pLen)
		return i - j;
	else
		return -1;
}

1.5.3 next数组的优化

这里引入一个串最小下标为1(前面聊过不同的串标准next数组的差异)的模式串对应的next数组:

image-20221030001134880

该模式串:第一个字符和第三个字符都是 ‘A’,它们对应的 next 值分别是 0 和 1;同样,第二个字符和第四个字符都是 ‘B’,它们对应的 next 值分别是 1 和 2。

第 3 个字符 ‘A’ 对应的 next 值是 1,意味着当该字符导致模拟匹配失败后,下次匹配会从模式串的第 1 个字符’A’ 开始。那么问题来了,第一次模式匹配失败说明模式串中的 ‘A’ 和主串对应的字符不相等,那么下次用第 1 个字符 ‘A’ 与该字符匹配,也绝不可能相等

同样的道理,第 4 个字符 ‘B’ 对应的 next 值是 2,意味着当该字符导致模式匹配失败后,下次模式匹配会从模式串第 2 个字符 ‘B’ 开始,这次匹配也绝不会成功。可以说这里做了些无用功。所以我们就思考如何优化next数组?

每次给next数组赋值前,当前字符和回溯后的字符是否相等!实现如下:

//优化过后的next 数组求法
void GetNextval(char* p, int next[])
{
	int pLen = strlen(p);
	next[0] = -1;
	int k = -1;
	int j = 0;
	while (j < pLen - 1)
	{
		//p[k]表示前缀,p[j]表示后缀  
		if (k == -1 || p[j] == p[k])
		{
			++j;
			++k;
			//较之前next数组求法,改动在下面4行
			if (p[j] != p[k])
				next[j] = k;   //之前只有这一行
			else
				//因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
				next[j] = next[k];
		}
		else
		{
			k = next[k];
		}
	}
}

相同的三元组以列标做升序排序。

#include<stdio.h>
#define NUM 10
//三元组
typedef struct {
    int i, j;
    int data;
}triple;
//三元组顺序表
typedef struct {
    triple data[NUM];
    int mu, nu, tu;
}TSMatrix;
//稀疏矩阵的转置
void transposeMatrix(TSMatrix M, TSMatrix* T) {
    //1.稀疏矩阵的行数和列数互换
    (*T).mu = M.nu;
    (*T).nu = M.mu;
    (*T).tu = M.tu;

    if ((*T).tu) {
        int col, p;
        int q = 0;
        //2.遍历原表中的各个三元组
        for (col = 1; col <= M.nu; col++) {
            //重复遍历原表 M.m 次,将所有三元组都存放到新表中
            for (p = 0; p < M.tu; p++) {
                //3.每次遍历,找到 j 列值最小的三元组,将它的行、列互换后存储到新表中
                if (M.data[p].j == col) {
                    (*T).data[q].i = M.data[p].j;
                    (*T).data[q].j = M.data[p].i;
                    (*T).data[q].data = M.data[p].data;
                    //为存放下一个三元组做准备
                    q++;
                }
            }
        }
    }
}

int main() {
    int i, k;
    TSMatrix M, T;
    M.mu = 3;
    M.nu = 2;
    M.tu = 4;

    M.data[0].i = 1;
    M.data[0].j = 2;
    M.data[0].data = 1;

    M.data[1].i = 2;
    M.data[1].j = 2;
    M.data[1].data = 3;

    M.data[2].i = 3;
    M.data[2].j = 1;
    M.data[2].data = 6;

    M.data[3].i = 3;
    M.data[3].j = 2;
    M.data[3].data = 5;

    for (k = 0; k < NUM; k++) {
        T.data[k].i = 0;
        T.data[k].j = 0;
        T.data[k].data = 0;
    }
    transposeMatrix(M, &T);
    for (i = 0; i < T.tu; i++) {
        printf("(%d,%d,%d)\n", T.data[i].i, T.data[i].j, T.data[i].data);
    }
    return 0;

2. 数组

编程语言中都提供有数组这种数据类型,比如 C/C++、Java 等。但本节我要讲解的不是作为数据类型的数组,而是数据结构中提供的一种叫数组的存储结构

数组是相同数据类型的集合,是线性表,用于存储一对一的数据。数组最大的特点就是结构固定–定义后维数和维界不再改变。我们经常选用顺序存储结构(顺序表)来实现数组,而不用链式结构(链表)。

数组可以是多维的,但存储数据元素的内存单元地址是一维的。n维数组就是n-1维数组的元素任然是一维数组,由此可见多位数组实际就是一维数组不断累加而来的。

2.1 数组的内存存储

数组可以是多维的,而顺序表只能是一维的线性空间。要想将 N 维的数组存储到顺序表中,可以采用以下两种方案:

以列序为主(先列后行):按照行号从小到大的顺序,依次存储每一列的元素;

image-20221027101738320

以行序为主(先行后序):按照列号从小到大的顺序,依次存储每一行的元素。

image-20221027101717819
  • 一维数组的内存存储

    image-20221027100636452
  • 二维数组的内存存储

    按行序为主序或列序为主序分为两种存储方式,多用行序为主序

    image-20221027101805643

    以行序为主序的数组:

    设数组开始存储位置LOC( 0,0) ,存储每个元素需要L个存储单元数组元素a[i][j]的存储位置是:``LOC( i , j)= LOC(o,0)+(n*i+j)*L`

    也就是首地址加上a[i][j]前面所有元素个数,即为a[i][j]地址位置

  • 三维数组的内存存储

    按页列行存放,页优先的顺序存储

    image-20221027102642736

    a[m1][m2] [m3]各维元素个数为m, m2, m3(页,行,列)
    下标为i1, i2 i3(页,行,列)的数组元素的存储位置:
    image-20221027103128724

    也就是首地址加上a[i][j]前面所有元素个数,即为a[i][j]地址位置

  • n维数组的内存存储

    image-20221027103716240

    各维数个数维m1,m2…mn的n维数组:
    L O C ( i 1 , I 2 , . . . , i n ) = a + ∑ j = 1 n − 1 i j ∗ ∏ k = j + 1 n m k + i n LOC(i_1,I_2,...,i_n)=a+\sum_{j=1}^{n-1}{i_j}*{\prod_{k=j+1}^{n}{m_k}}+i_n LOC(i1,I2,...,in)=a+j=1n1ijk=j+1nmk+in
    理解:在n维数组中求某个元素位置:就是求该元素前面有多少个元素+首地址

2.2 特殊矩阵

矩阵对元素随机存取的特性就要求我们要用数组存储它,数组存储矩阵的存储密度为1。

对特殊矩阵我们用一维数组存储特殊矩阵,对相同元素值只分配一个元素空间,对零元素不分配空间,达到矩阵压缩的目的。这里所说的特殊矩阵,主要分为以下两类:

  • 含有大量相同数据元素的矩阵,比如对称矩阵

  • 含有大量 0 元素的矩阵,比如稀疏矩阵、上(下)三角矩阵

2.2.1 对称矩阵

image-20221027110903909

矩阵中有两条对角线,其中图 中的对角线称为主对角线,另一条从左下角到右上角的对角线为副对角线。对称矩阵指的是各数据元素沿主对角线对称的矩阵。

结合数据结构压缩存储的思想,我们可以使用一维数组存储对称矩阵。由于矩阵中沿对角线两侧的数据相等,因此数组中只需存储对角线一侧(包含对角线)的数据即可。

对于n维数组,二维数组存储需n2个存储空间,但矩阵压缩用一维数组只需要n(n+1)/2个元素空间。

对称矩阵压缩:将对称矩阵的下三角矩阵(或上三角矩阵)以行序为主序展开存在一维数组中

image-20221027111119650

原对称矩阵中下三角矩阵元素a[i][j]在一维数组中位置(以下三角为例):
k = i ( i − 1 ) 2 + j + a / / a 是 一 维 数 组 首 地 址 k=\frac{i(i-1)}{2}+j+a//a是一维数组首地址 k=2i(i1)+j+a//a
而原上三角矩阵元素a[i][j]在一维数组中的位置(对应到下三角矩阵的元素即为a[j][i],高中知识)
k = j ( j − 1 ) 2 + i + a / / a 是 一 维 数 组 首 地 址 k=\frac{j(j-1)}{2}+i+a//a是一维数组首地址 k=2j(j1)+i+a//a
就是首地址加上a[i][j]前面所有元素个数,即为a[i][j]地址位置

2.2.2 上(下)三角矩阵

image-20221027111958819

主对角线下的数据元素全部相同的矩阵为上三角矩阵(图 a),主对角线上元素全部相同的矩阵为下三角矩阵(图 4b)。

矩阵压缩的方式类似对称矩阵,不在此赘述了。重复元素c单独共享一个元素存储空间,其余的,将对称矩阵的下三角矩阵(或上三角矩阵)以行序为主序展开存在一维数组中,所以n维数组存储上下三角矩阵需要n(n+1)/2+1个元素空间

2.2.3 对角矩阵

[特点]在n阶的方阵中,所有非零元素都集中在以主对角线为中心的带状区域中,区域外的值全为0,则称为对角矩阵。常见的有三对角矩阵、五对角矩阵、七对角矩阵等。(数字代表对角矩阵的对角线数量)

按对角线来存储

image-20221027122950997

坐标对应关系:原矩阵a[i][j]映射到五对角矩阵中为a[j-i][j]

2.2.4 稀疏矩阵

image-20221027113026717

如果矩阵中分布有大量的元素 0,即非 0 元素非常少,这类矩阵称为稀疏矩阵。(当非零元素比所有元素小于等于0.05)

压缩存储稀疏矩阵的方法是:只存储矩阵中的非 0 元素,与前面的存储方法不同,稀疏矩阵非 0 元素的存储需同时存储该元素所在矩阵中的行标和列标。

2.2.4.1 三元组顺序表存储稀疏矩阵

用一个顺序表存储稀疏矩阵,顺组表的元素是一个三元组,分别记录非零元素的横坐标、纵坐标、和数值。为了矩阵的还原,通常会在顺序表表头加上矩阵信息,记录总行数、总列数、非零元素总个数。对于矩阵

image-20221027154511475

其三元顺序表为:

image-20221027154855635

反过来我们也可以根据稀疏矩阵的三元顺序表得到系数矩阵

三元组顺序表优点:非零元在表中按行序有序存储,因此便于进行依行顺序处理的矩阵运算。

三元顺序表的缺点:不能随机存取,若按行号存取某一行的非零元则需从头开始进行查找。如三元顺序表因矩阵运算可能导致增加或减少零元素,三元组顺序表插入删除操作需要遍历或移动大量数据。

2.2.4.2 十字链表存储稀疏矩阵

十字链表中,矩阵非零元素用结点表示,节点存储矩阵位置及数值(row,col,value),此外还有right和down两个域

  • right:用于链接同一行中的下一个非零元素;
  • down:用以链接同一列中的下一个非零元素。

十字链表存储矩阵结点示意图

image-20221027164731118

对于矩阵:

image-20221027164805889

十字链表对应

image-20221027165010888

十字链表会用到两数组:行数组和列数组。(方便随机存取,也能用链表)

行数组记录每一行第一个非零元素地址,列数组记录每一列第一个非零元素地址。

将非零元素封装在结点中,记录行坐标,列坐标,元素数值,当前行下一个非零元素,当前列下一个非零元素。

简述十字链表代码实现:先开辟好行数组和列数组,数组初值均为null。然后遍历稀疏矩阵,遇到非零元素就将非零元素封装在结点中(down和right初值均为零),封装好后就链接到行列数组中,若行列数组对应位置为null,则直接链接上;若行列数组非空就依次找到行列中相应的down和right域为空的位置,链接上,如此往复就能实现十字链表的存储。

下面是代码实现:

//用于存储非零元素的结点
typedef struct OLNode{
    int i,j;//元素的行标和列标
    int data;//元素的值
    struct OLNode * right,*down;//两个指针域
    //当前行写个非零元素,当前列下个非零元素
}OLNode, *OLink;

//表示十字链表的结构体:行列数组
typedef struct
{
    OLink *rhead, *chead; //行和列链表头指针
    int mu, nu, tu;  //矩阵的行数,列数和非零元的个数
}CrossList;

//稀疏矩阵转化为十字链表
void CreateMatrix_OL(CrossList* M);
//输出十字链表
void display(CrossList M);

int main()
{
    CrossList M;
    M.rhead = NULL;
    M.chead = NULL;
    CreateMatrix_OL(&M);

    printf("输出矩阵M:\n");
    display(M);
    return 0;
}

//稀疏矩阵转化为十字链表代码实现
void CreateMatrix_OL(CrossList* M)
{
    int m, n, t;
    int num = 0;
    int i, j, e;
    OLNode* p = NULL, * q = NULL;
    printf("输入矩阵的行数、列数和非0元素个数:");
    scanf("%d%d%d", &m, &n, &t);
    (*M).mu = m;
    (*M).nu = n;
    (*M).tu = t;

    if (!((*M).rhead = (OLink*)malloc((m + 1) * sizeof(OLink))) || !((*M).chead = (OLink*)malloc((n + 1) * sizeof(OLink))))
    {
        printf("初始化矩阵失败");
        exit(0);
    }
   
    for (i = 0; i <= m; i++)
    {
        (*M).rhead[i] = NULL;
    }
    for (j = 0; j <= n; j++)
    {
        (*M).chead[j] = NULL;
    }

    while (num < t) {
        scanf("%d%d%d", &i, &j, &e);
        num++;
        if (!(p = (OLNode*)malloc(sizeof(OLNode))))
        {
            printf("初始化三元组失败");
            exit(0);
        }
        p->i = i;
        p->j = j;
        p->e = e;

        //链接到行的指定位置
        //如果第 i 行没有非 0 元素,或者第 i 行首个非 0 元素位于当前元素的右侧,直接将该元素放置到第 i 行的开头
        if (NULL == (*M).rhead[i] || (*M).rhead[i]->j > j)
        {
            p->right = (*M).rhead[i];
            (*M).rhead[i] = p;
        }
        else
        {
            //找到当前元素的位置
            for (q = (*M).rhead[i]; (q->right) && q->right->j < j; q = q->right);
            //将新非 0 元素插入 q 之后
            p->right = q->right;
            q->right = p;
        }

        //链接到列的指定位置
        //如果第 j 列没有非 0 元素,或者第 j 列首个非 0 元素位于当前元素的下方,直接将该元素放置到第 j 列的开头
        if (NULL == (*M).chead[j] || (*M).chead[j]->i > i)
        {
            p->down = (*M).chead[j];
            (*M).chead[j] = p;
        }
        else
        {
            //找到当前元素要插入的位置
            for (q = (*M).chead[j]; (q->down) && q->down->i < i; q = q->down);
            //将当前元素插入到 q 指针下方
            p->down = q->down;
            q->down = p;
        }
    }
}
2.2.4.3 稀疏矩阵逆置

举证的转置,对于二维数组存储的矩阵,就是将a[i][j]换到a[j][i]的位置。很简单,但是对于稀疏矩阵,0元素很多,赋值没有什么意义,而且数组的存储有些浪费空间。所以我们讨论的是三元组法存储的稀疏矩阵

稀疏矩阵转置思路

  1. 将稀疏矩阵的行数和列数互换;
  2. 将三元组表(存储矩阵)中的 i 列和 j 列互换,实现矩阵的转置;
  3. 以 j 列为序,重新排列三元组表中存储各三元组的先后顺序;

三元组顺序表中,各个三元组会以行标做升序排序,行标相同的三元组以列标做升序排序。

#include<stdio.h>
#define NUM 10
//三元组
typedef struct {
    int i, j;
    int data;
}triple;
//三元组顺序表
typedef struct {
    triple data[NUM];
    int mu, nu, tu;
}TSMatrix;
//稀疏矩阵的转置
void transposeMatrix(TSMatrix M, TSMatrix* T) {
    //1.稀疏矩阵的行数和列数互换
    (*T).mu = M.nu;
    (*T).nu = M.mu;
    (*T).tu = M.tu;

    if ((*T).tu) {
        int col, p;
        int q = 0;
        //2.遍历原表中的各个三元组
        for (col = 1; col <= M.nu; col++) {
            //重复遍历原表 M.m 次,将所有三元组都存放到新表中
            for (p = 0; p < M.tu; p++) {
                //3.每次遍历,找到 j 列值最小的三元组,将它的行、列互换后存储到新表中
                if (M.data[p].j == col) {
                    (*T).data[q].i = M.data[p].j;
                    (*T).data[q].j = M.data[p].i;
                    (*T).data[q].data = M.data[p].data;
                    //为存放下一个三元组做准备
                    q++;
                }
            }
        }
    }
}

int main() {
    int i, k;
    TSMatrix M, T;
    M.mu = 3;
    M.nu = 2;
    M.tu = 4;

    M.data[0].i = 1;
    M.data[0].j = 2;
    M.data[0].data = 1;

    M.data[1].i = 2;
    M.data[1].j = 2;
    M.data[1].data = 3;

    M.data[2].i = 3;
    M.data[2].j = 1;
    M.data[2].data = 6;

    M.data[3].i = 3;
    M.data[3].j = 2;
    M.data[3].data = 5;

    for (k = 0; k < NUM; k++) {
        T.data[k].i = 0;
        T.data[k].j = 0;
        T.data[k].data = 0;
    }
    transposeMatrix(M, &T);
    for (i = 0; i < T.tu; i++) {
        printf("(%d,%d,%d)\n", T.data[i].i, T.data[i].j, T.data[i].data);
    }
    return 0;
}

3.广义表

3.1 广义表的概念及性质

广义表是线性表的推广,也称列表。

广义表记作:LS=(a1, … , an

在线性表中ai只限于单个元素,广义表中ai可以是单个元素也可以是广义表。习惯上用大写字母表示广义表,小写字母表示原子。

广义表常见形式

  • A = ():A 空表,长度为0
  • B = (e):广义表 B 中只有一个原子 e,长度为1。
  • C = (a,(b,c,d)) :广义表 C 中有两个元素,原子 a 和子表 (b,c,d),长度为2。
  • D = (A,B,C):广义表 D 中存有 3 个子表,分别是A、B和C。这种表示方式等同于 D = ((),(e),(b,c,d)) 。
  • E = (a,E):广义表 E 中有两个元素,原子 a 和它本身。这是一个递归广义表,等同于:E = (a,(a,(a,…)))。
  • 注意,A = () 和 A = (()) 是不一样的。前者是空表,而后者是包含一个子表的广义表,只不过这个子表是空表。

具上推出重要结论:

  1. 广义表的元素可以是子表,子表的元素还可以是子表…所以广义表是一个多层次结构可以用图形象的表示,如:A = (),B = (e),C = (a,(b,c,d)) ,D = (A,B,C)

    image-20221030230403128
  2. 广义表可以为其他广义表所共享;如:广义表B共享表A,在B中不必列出A的值,而是通过名称来引用:B=(A)

  3. 广义表可以是一个递归的表,即广义表也可以是其本身的一个子表。如:E=(a,E)

广义表的表头和表尾:

当广义表不是空表时,称第一个数据(原子或子表)为"表头",剩下的数据构成子表则为"表尾"。所以非空广义表的表尾一定是一个广义表

广义表的性质:

  • 广义表中的数据元素有相对次序:一个直接前驱和一个直接后继

  • 广义表的长度:最外层所包含的数据元素的个数

    如:C=(a,(b,c))是一个长度为2的广义表

  • 广义表的深度:广义表完全展开后所包含括号的重数:

    A = ()深度为1,B = (e)深度为1,C = (a,(b,c,d))深度为2,D = (A,B,C)深度为3

3.2 广义表的存储结构

广义表中的数据元素可以由不同的结构(或原子或列表),所以难以用顺序存储结构,通常用链式存储结构。常用的链式存储结构由两种,头尾链表的存储结构和扩展线性表的存储结构

3.2.1 头尾链表的存储结构

广义表中的数据元素可能为原子或列表,由此需要两种结构的结点:

  • 一种是表结点,用以表示列表
  • 一种是原子结点,用以表示原子。

前面说过,若列表不空,则可分解成表头和表尾。反之,一对确定的表头和表尾可惟一确定列表。

  • 表结点可由3个域组成:标志域、指示表头的指针域和指示表尾的指针域
  • 原子结点只需两个 域:标志域和值域(如图5.8所示)。其形式定义说明如下:
image-20221030225613756
typedef struct GLNode {
    int tag;//标志域区分原子结点还是表结点
    
    union {
        AtomType atom;//原子结点的值域,Atom是原子结点类型
        
        struct {
            struct Node* hp, * tp;
        }ptr;//表结点的指针域,hp指向表头;tp指向表尾
    };
}* Glist;

对应的A = (),B = (e),C = (a,(b,c,d)) ,D = (A,B,C),该广义表的存储结构如下:

image-20221030230442797

而具体代码实现则需要一个个手写,就不在此赘述了

头尾链表的存储结构有如下性质:

  1. 除空表的表头指针为空外,对任何非空列表,其表头指针均指向一个表结点,且该结点中的hp域指示列表表头(或为原子结点,或为表结点), tp域指向列表表尾(除非表尾为空,则指针为空,否则必为表结点)
  2. 容易分清列表中原子和子表所在层次。如在列表D中,原子a和e在同一层次上,而b、c和d在同一层次且比a和e低一层,B和C是同-层的子表
  3. 最高层的表结点个数即为列表的长度。

以上3个特点在某种程度上给列表的操作带来方便。也可采用另一种结点结构的链表表示列表

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/27934.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[ 常用工具篇 ] 解决kali英文操作不方便的问题 -- kali 设置中文界面

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

iNFTnews|FTX一夜崩塌,但Web3仍前途光明

元宇宙的日子越来越不好过了。 FTX的暴雷仍在产生广泛的影响&#xff0c;以太坊的价格快跌到1000美元了&#xff0c;这与去年11月4900美元的历史新高形成鲜明对比。 不过&#xff0c;尽管市场低迷&#xff0c;创作者仍然在Web3领域找到了爱与支持&#xff0c;甚至是可持续发展…

正则表达式快速入门

目录1.正则表达式是什么&#xff0c;有什么作用2.定位符例子2.1想要搜索以“001”开头的文件2.2想要搜索以“ab”结尾的文件2.3搜索单词开头为“zh"的文件2.4搜索单词结尾为“zh"的文件2.5搜索单词中间为“zh"的文件3.限定符例子3.1 搜索以“0”开头&#xff0c…

【Call for papers】DSN-2023(CCF-B/软件工程/2022年12月7日截稿)

文章目录1.会议信息2.时间节点3.论文主题On behalf of the Organizing Committee, we extend you a warm welcome to the 53rd Annual IEEE/IFIP International Conference on Dependable Systems and Networks (DSN 2023), organized by the University of Coimbra, Portugal.…

MyBatis-Plus 联表查询

文章目录前言引入依赖数据准备修改 Mapper查询分页查询前言 它的联表查询能力一直被大家所诟病。一旦遇到 left join 或 right join 的左右连接&#xff0c;你还是得老老实实的打开 xml 文件&#xff0c;手写上一大段的 sql 语句。 直到前几天&#xff0c;偶然碰到了这么一款…

麻了,3个offer不知道选哪个?

有的小伙伴苦于没offer&#xff0c;有的小伙伴苦于offer多&#xff0c;不知道选择哪个&#xff1f; 本科双非&#xff0c;硕士末端985&#xff0c;拿到了三个offer&#xff0c;过来问小孟去哪&#xff1f; 一&#xff0c;拿到三个不错的offer&#xff1a; 三个offer分别是阿里…

JDK8新特性之Stream流

目录 集合处理数据的弊端 Stream流的获取方式 对于Collection的实现类 对于Map 对于数组 Stream常用方法介绍 count forEach filter limit skip map sorted distinct match find max和min reduce mapToInt concat Stream结果收集 结果收集到集合中 结果收…

分布式定时调度-xxl-job

分布式定时调度-xxl-job 一.定时任务概述 1.定时任务认识 1.1.什么是定时任务 定时任务是按照指定时间周期运行任务。使用场景为在某个固定时间点执行&#xff0c;或者周期性的去执行某个任务&#xff0c;比如&#xff1a;每天晚上24点做数据汇总&#xff0c;定时发送短信等。 …

Android中Adapter的作用

Adapter的介绍 An Adapter object acts as a bridge between an AdapterView and the underlying data for that view. The Adapter provides access to the data items. The Adapter is also responsible for making aView for each item in the data set. 一个Adapter是Ada…

关于 SAP Cloud Connector 500 failed to sign the Certificate 的错误消息

有朋友向我询问一个关于 SAP Cloud Connector 的问题&#xff0c;错误消息如下&#xff1a; 500 failed to sign the Cloud Connector Certificate for subaccount XXX. Verify Configuration and proxy settings. See Log And Trace Files and in particular ljs_trace.log fo…

基于Java+SpringBoot+Mybatis+Vue+ElementUi的校园闲置物品交易

项目介绍 我们通过java语言&#xff0c;后端springboot框架&#xff0c;数据库mysql&#xff0c;前端vue技术&#xff0c;开发本系统&#xff0c;校园闲置物品交易网站系统中的功能模块主要是实现管理员&#xff1b;首页、个人中心、用户管理、商品类型管理、商品信息管理、系…

什么是固话号码认证?固话号码认证有用吗?

固话号码认证提供企业号码认证服务&#xff0c;来电场景中展现企业LOGO&#xff0c;展现品牌&#xff0c;可以查看更多企业相关信息&#xff0c;可有效提高接通率&#xff0c;保证品牌企业的身份及商业价值。 那如何实施号码认证服务呢&#xff1f;接下来小编就给大家整理了号…

ICML-2022 | 强化学习论文清单(附链接)

第39届国际机器学习会议&#xff08;International Conference on Machine Learning, ICML 2022&#xff09;于北京时间7月17日至7月23日&#xff0c;在美国马里兰州巴尔的摩市以线上线下结合的方式举办。 本文列举了会议主题与强化学习&#xff08;Reinforcement Learning, R…

20行Python代码,轻轻松松获取各路书本,你还在花钱买着看嘛~

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 又到了学Python时刻~ 作为现代青年&#xff0c;我相信应该没几个没看过xiao shuo的吧&#xff0c;嘿嘿~ 一般来说咱们书荒的时候怎么办&#xff1f; 自然是去寻一个网站先找到xiao shuo名字&#xff0c;然后再找度娘…

高压功率放大器原理和应用场合介绍

高压功率放大器是用来对&#xff08;脉冲发生器、函数发生器以及波形发生器等&#xff09;信号进行电压、电流或者功率放大&#xff0c;能够给测试负载提供信号驱动的测量仪器。独立的波形功率放大器分为电压放大器、电流放大器以及电压/电流放大器。另外&#xff0c;还有一类被…

Spring的常用拓展点

文章目录自定义拦截器获取 Spring 容器对象修改 BeanDefinition添加BeanDefinition测试初始化 Bean 前后初始化方法使用PostConstruct 注解实现 InitializingBean 接口BeanFactoryPostProcessor 接口关闭容器前自定义作用域自定义拦截器 spring mvc 拦截器的顶层接口是&#x…

web端常见导航设计

一、导航的定义 导航作为网站或者平台的骨架&#xff0c;是产品设计中不容忽视的一环导航是内容或者功能的定位、导向与通道。 二、导航分类 遵循导航层级结构&#xff0c;包括全局导航和局部导航 全局导航往往指页眉和页脚&#xff0c;存在于网站的大部分页面&#xff0c;便于…

游戏引擎概述-Part1

一、简述自己的学习心路历程 自从业UNITY以来已经有4个月多了&#xff0c;回想起来自己从工作以来就很少写博客了&#xff0c;也算督促一下自己&#xff0c;回想自己从最早的Unity开始&#xff0c;入手C#和编辑器、Unity开发界面&#xff0c;再到自己学一些Unity的小项目…

有效学习,通过PMP考试

但是我们时间有限&#xff0c;如何有效利用这些资料&#xff0c;花最少时间通过考试&#xff0c;是个关键问题。 课程主要的资料包括&#xff1a; PMBOK。官方指定用书&#xff0c;考试知识点来自PMBOK。 汪博士解读PMP考试。考试参考书&#xff0c;比PMBOK解析得更清楚&…

Qt-FFmpeg开发-视频播放(3)

Qt-FFmpeg开发-视频播放【软解码 OpenGL显示RGB图像】 文章目录Qt-FFmpeg开发-视频播放【软解码 OpenGL显示RGB图像】1、概述2、实现效果3、FFmpeg软解码流程4、主要代码4.1 解码代码4.2 OpenGL显示RGB图像代码5、完整源代码更多精彩内容&#x1f449;个人内容分类汇总 &…