矩阵的三种存储方式---三元组法 行逻辑链接法 十字链表法

  在介绍矩阵的压缩存储前,我们需要明确一个概念:对于特殊矩阵,比如对称矩阵,稀疏矩阵,上(下)三角矩阵,在数据结构中相同的数据元素只存储一个。

三元组顺序表

  稀疏矩阵由于其自身的稀疏特性,通过压缩可以大大节省稀疏矩阵的内存代价。具体操作是:将非零元素所在的行、列以及它的值构成一个三元组(i,j,v),然后再按某种规律存储这些三元组,这种方法可以节约存储空间 。
  如下图所示为一个稀疏矩阵,我们应该怎么样存储呢?

在这里插入图片描述


  若对其进行压缩存储,我们可以将一个非零数组元素的三元看成一个单位存入一维数组,具体如下所示。比如(1,1,1)代表第一行第一列的元素值为1。注意,这里我们只存储非零值。

在这里插入图片描述


  具体代码如下:

#include<stdio.h>
#include <stdlib.h>
#include <time.h>
#define NUMBER  3

//三元组结构体
typedef struct {
    //行标r,列标c
    int r,c;
    //元素值
    int data;
}triple;
//矩阵的结构表示
typedef struct {
    //存储该矩阵中所有非0元素的三元组
    triple data[NUMBER];
    //row和column分别记录矩阵的行数和列数,num记录矩阵中所有的非0元素的个数
    int row,column,num;
}TSMatrix;
//输出存储的稀疏矩阵
void print(TSMatrix M);
int main() {
    int i;
    srand((unsigned)time(NULL));
    TSMatrix M;
    M.row=3;
    M.column=3;
    M.num=3;
    //初始化矩阵
    for(i=0;i<M.num;i++){
        //随机数范围[1,3]
        M.data[i].r=rand()%M.num+1;
        M.data[i].c=rand()%M.num+1;
        M.data[i].data=rand()%10;
    }
    print(M);
    return 0;
}
void print(TSMatrix M){
    for(int i=1;i<=M.row;i++){
        for(int j=1;j<=M.column;j++){
            int value =0;
            for(int k=0;k<M.num;k++){
                //遍历时的r,c行列值和实际存储row,column行列值的比较,相同的话就说明有非零元素,打印存储的非0值
                if(i == M.data[k].r && j == M.data[k].c){
                    printf("%d ",M.data[k].data);
                    value =1;
                    break;
                }
            }
            if(value == 0)
                printf("0 ");
        }
        printf("\n");
    }
}

行逻辑链接的顺序表

  使用三元组顺序表存储稀疏矩阵,我们每次访问其中一个元素都要遍历整个矩阵,效率比较低。我们可以使用一个一维数组来存储每行第一个非零元素在一维数组中的位置,这样就可以提升访问效率。这样的表就叫做行逻辑链接的顺序表。
  下图为一个稀疏矩阵,当使用行逻辑链接的顺序表对其进行压缩存储时,需要做以下两个工作:

在这里插入图片描述


  1.将矩阵中的非 0 元素采用三元组的形式存储到一维数组 data 中:

在这里插入图片描述


  2.使用数组 rpos 记录矩阵中每行第一个非 0 元素在一维数组中的存储位置。

在这里插入图片描述


  通过以上两步操作,即实现了使用行逻辑链接的顺序表存储稀疏矩阵。
  此时,如果想从行逻辑链接的顺序表中提取元素,则可以借助 rpos 数组提高遍历数组的效率。
  例如,提取图 1 稀疏矩阵中的元素 2 的过程如下:
  由 rpos 数组可知,第一行首个非 0 元素位于data[1],因此在遍历此行时,可以直接从第 data[1] 的位置开始,一直遍历到下一行首个非 0 元素所在的位置(data[3])之前;
  同样遍历第二行时,由 rpos 数组可知,此行首个非 0 元素位于 data[3],因此可以直接从第 data[3] 开始,一直遍历到下一行首个非 0 元素所在的位置(data[4])之前;遍历第三行时,由 rpos 数组可知,此行首个非 0 元素位于 data[4],由于这是矩阵的最后一行,因此一直遍历到 rpos 数组结束即可(也就是 data[tu],tu 指的是矩阵非 0 元素的总个数)。
具体代码如下

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAXSIZE 12500
#define MAXRC 100
typedef struct
{
    //行,列
    int r,c;
    //元素值
    int e;
}Triple;

typedef struct
{
    //矩阵中元素的个数
    Triple  data[MAXSIZE];
    //每行第一个非零元素在data数组中的位置
    int rpos[MAXRC];
    //行数,列数,元素个数
    int row,number;
}RLSMatrix;
//矩阵的输出函数
void print(RLSMatrix M){
    for(int i=1;i<=M.row;i++){
        for(int j=1;j<=M.column;j++){
            int value=0;
            if(i+1 <=M.row){
                for(int k=M.rpos[i];k<M.rpos[i+1];k++){
                    if(i == M.data[k].r && j == M.data[k].c){
                        printf("%d ",M.data[k].e);
                        value=1;
                        break;
                    }
                }
                if(value==0){
                        printf("0 ");
                    }
            }else{
                for(int k=M.rpos[i];k<=M.number;k++){
                    if(i == M.data[k].r && j == M.data[k].c){
                        printf("%d ",M.data[k].e);
                        value=1;
                        break;
                    }

                }
                if(value==0){
                    printf("0 ");
                }
            }

        }
        printf("\n");
    }
}
int main()
{
    int i;
   //srand((unsigned)time(NULL));

    RLSMatrix M;
    //矩阵的大小
    M.number = 4;
    M.row = 3;
    M.column = 4;
    //每一行首个非零元素在一维数组中的位置
    M.rpos[1] = 1;
    M.rpos[2] = 3;
    M.rpos[3] = 4;

    M.data[1].e = 3;
    M.data[1].r = 1;
    M.data[1].c = 2;

    M.data[2].e = 5;
    M.data[2].r = 1;
    M.data[2].c = 4;

    M.data[3].e = 1;
    M.data[3].r = 2;
    M.data[3].c = 3;

    M.data[4].e = 2;
    M.data[4].r = 3;
    M.data[4].c = 1;
    //输出矩阵
    print(M);
    return 0;
}

十字链表法

  关于十字链表法,具体可看下图。我们把矩阵的每一行每一列分别看成一个链表,然后将每一行和每一列的链表的第一个元素存放在一个数组中。这个数组就叫行链表的头指针数组,列链表的头指针数组。当我们访问矩阵的时候,就可以从行/列头指针数组中取出对应的指针,就可以访问这一行或者这一列的元素了。

在这里插入图片描述


在这里插入图片描述

  链表中节点的结构应如下图。所以,除了定义三元组的行,列,数值外,我们还需要定义指向行的指针,指向列的指针。最后还需要定义一个存放行/列链表头结点的数组专门存放各行各列的头结点。具体代码如下。

在这里插入图片描述

typedef struct CLNode
{
    //矩阵三元组i代表行 j代表列 e代表当前位置的数据
    int r,c,data; 
    //指针域 行指针 列指针
    struct CLNode *prow,*pcolumn; 
}CLNode,*CLink;
typedef struct
{
    //行和列链表头数组  CLink rhead[] 这样写也可以。写成指针是为了方便动态分配内存
    CLink *rhead,*chead; 
    //矩阵的行数,列数和非零元的个数
    int rows,columns,num;  
}CrossList;

  下面我们将要根据用户输入的行数,列数,非零元素的值,来创建矩阵。

//注意检查用户的输入
 do {  
        flag = 1;      
    printf("输入矩阵的行数、列数和非0元素个数:");
    scanf("%d%d%d",&m,&n,&t);
    if (m<0 || n<0 || t<0 || t>m*n)  
            flag = 0;  
    }while (!flag);  
    M.rows = m;
    M.columns = n;
    M.num = t;

  用户输入合法的情况下我们要创建并初始化存放行列链表头的数组。

    //因为下标从1开始,所以头结点指针多分配一个内存
    if (!(M.rhead = (CLink*)malloc((m + 1) * sizeof(CLink))) || !(M.chead = (CLink*)malloc((n + 1) * sizeof(CLink))))
    {
        printf("初始化矩阵失败\r\n");
        exit(0);
    }
    // 初始化行头指针向量;各行链表为空链表 
    for (r = 1; r <= m; r++)
    {
        M.rhead[r] = NULL;
    }
    // 初始化列头指针向量;各列链表为空链表    
    for (c = 1; c <= n; c++)
    {
        M.chead[c] = NULL;
    }

  存放行列链表头的数组准备好了,接下来我们就要创建数据节点了。根据用户输入的行号,列好,数值创建节点。这里同样要检查用户的输入。

for (scanf("%d%d%d",&r,&c,&data); ((r<=0)||(c<=0)); scanf("%d%d%d",&data))
{
       if (!(p = (CLNode*)malloc(sizeof(CLNode))))
       {
           printf("初始化三元组失败");
           exit(0);
       }
        p->r = r;
        p->c = c;
        p->data= data;
}

  当创建好一个节点之后,我们就要放到行或者列的正确的位置。根据输入的行号列号确定插入的位置。那么应该怎样去插入?分两种情况
  1、当这一行中没有结点的时候,那么我们直接插入
  2、当这一行中有结点的时候我们插入到正确的位置
  对于第一个问题,因为之前已经对头结点数组进行了初始化NULL,所以直接根据NULL==M->rhead[i]就可以判断一行中有没有节点。
  对于第二个问题,当行中有节点的时候,无非是插入到某个节点之前或者某个节点之后。什么时候插入到节点前?什么时候插入到节点后呢?
  1.插入节点前:当我们要插入的节点的列号小于已经存在的节点的列号,这个时候就要插入到这个节点之前了。
  2.插入节点后:当我们要插入的节点的列号大于已经存在的节点的列号,这个时候就要插入到这个节点之后了。
  对于第一种情况,代码如下。

//p为准备插入的节点,要插入到M.rhead[r]之前。
      if (NULL == M.rhead[r] || M.rhead[r]->c> c)
           {
               p->prow = M.rhead[r];
               //M.rhead[r]要始终指向行的第一个节点
               M.rhead[r] = p;
           }

  对于第二种情况,我们要插入的节点插入到已有节点的后面,那么,已有将要插入节点的列号必定大于已有节点的列号。我们只要找到一个节点比我们将要插入节点的列号大的节点就好,然后插入到这个节点的前面。如果现有的结点没有一个结点列号是大于要插入的节点的列号的,那么我们就应该插入到最后一个结点之后!

              //我们要找到一个比q节点大的节点。在这个节点之前插入
               for (q = M.rhead[r]; (q->prow) && q->prow->c < c; q = q->prow);
               p->prow = q->prow;
               q->prow = p;

  对于列的插入同样如此,就不一一分析了,下面给出具体代码。

       //链接到列的指定位置
        if (NULL == M.chead[c] || M.chead[c]->r> r)
        {
            p->pcolumn = M.chead[c];
            M.chead[c] = p;
        }
        else
        {
            for (q = M.chead[c]; (q->pcolumn) && q->pcolumn->r < r; q = q->pcolumn);
            p->pcolumn = q->pcolumn;
            q->pcolumn = p;
        }     

  打印矩阵
对于十字链表矩阵的打印,我们每次从行/列头结点数组中取出每一行或者每一列的第一个节点依次往下访问就可以了,和普通的链表访问没有区别。如果对链表不熟悉的可以参考这篇文章史上最全单链表的增删改查反转等操作汇总以及5种排序算法(C语言)

void PrintClist(CrossList M)
{
  for (int i = 1; i <= M.num; i++)
    {
        if (NULL != M.chead[i])
        {
            CLink p = M.chead[i];
            while (NULL != p)
            {
                printf("%d\t%d\t%d\n",p->r,p->c,p->data);
                p = p->pcolumn;
            }
        }
    }   
}

  完整代码如下:

/*
 * @Description: 十字链表存储压缩矩阵
 * @Version: V1.0
 * @Autor: Carlos
 * @Date: 2020-05-26 16:43:48
 * @LastEditors: Carlos
 * @LastEditTime: 2020-05-28 14:40:19
 */ 
#include<stdio.h>
#include<stdlib.h>
typedef struct CLNode
{
    //矩阵三元组i代表行 j代表列 e代表当前位置的数据
    int r,*CLink;
typedef struct
{
    //行和列链表头指针
    CLink *rhead,num;  
}CrossList;
CrossList InitClist(CrossList M)
{
    CLNode *p,*q;
    int r,data;
    int m,n,t;
    int flag;

    //  printf("输入矩阵的行数、列数和非0元素个数:");
    // scanf("%d%d%d",&t);
 do {  
        flag = 1;      
    printf("输入矩阵的行数、列数和非0元素个数:");
    scanf("%d%d%d",&t);
    if (m<0 || n<0 || t<0 )  
            flag = 0;  
    }while (!flag);  
    M.rows = m;
    M.columns = n;
    M.num = t;
    //因为下标从1开始,所以头结点指针多分配一个内存
    if (!(M.rhead = (CLink*)malloc((m + 1) * sizeof(CLink))) || !(M.chead = (CLink*)malloc((n + 1) * sizeof(CLink))))
    {
        printf("初始化矩阵失败\r\n");
        exit(0);
    }
    // 初始化行头指针向量;各行链表为空链表 
    for (r = 1; r <= m; r++)
    {
        M.rhead[r] = NULL;
    }
    // 初始化列头指针向量;各列链表为空链表    
    for (c = 1; c <= n; c++)
    {
        M.chead[c] = NULL;
    }
//行数列数不为0
for (scanf("%d%d%d",&data))
{
       if (!(p = (CLNode*)malloc(sizeof(CLNode))))
       {
           printf("初始化三元组失败");
           exit(0);
       }
        p->r = r;
        p->c = c;
        p->data= data;
      //链接到行的指定位置。 
      if (NULL == M.rhead[r] || M.rhead[r]->c> c)
           {
               p->prow = M.rhead[r];
               M.rhead[r] = p;
           }
           else
           {
               for (q = M.rhead[r]; (q->prow) && q->prow->c < c; q = q->prow);
               p->prow = q->prow;
               q->prow = p;
           } 
         //链接到列的指定位置
        if (NULL == M.chead[c] || M.chead[c]->r> r)
        {
            p->pcolumn = M.chead[c];
            M.chead[c] = p;
        }
        else
        {
            for (q = M.chead[c]; (q->pcolumn) && q->pcolumn->r < r; q = q->pcolumn);
            p->pcolumn = q->pcolumn;
            q->pcolumn = p;
        }     
 
    }
     return M;
}

void PrintClist(CrossList M)
{
  for (int i = 1; i <= M.num; i++)
    {
        if (NULL != M.chead[i])
        {
            CLink p = M.chead[i];
            while (NULL != p)
            {
                printf("%d\t%d\t%d\n",p->data);
                p = p->pcolumn;
            }
        }
    }   
}
int main()
{
    CrossList M;
    M.rhead = NULL;
    M.chead = NULL;
    M = InitClist(M);
    PrintClist(M);
    return 0;
}

  文中代码均已测试,有任何意见或者建议均可联系我。欢迎学习交流!
  如果觉得写的不错,请点个赞再走,谢谢!

有任何问题,均可通过公告中的二维码联系我

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


【啊哈!算法】算法3:最常用的排序——快速排序       上一节的冒泡排序可以说是我们学习第一个真正的排序算法,并且解决了桶排序浪费空间的问题,但在算法的执行效率上却牺牲了很多,它的时间复杂度达到了O(N2)。假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序则只需要0.1秒,而冒泡排序则需要1千万秒,达到115天之久,是不是很吓人。那有没有既不浪费空间又可以快一点的排序算法
匿名组 这里可能用到几个不同的分组构造。通过括号内围绕的正则表达式就可以组成第一个构造。正如稍后要介绍的一样,既然也可以命名组,大家就有考虑把这个构造作为匿名组。作为一个实例,请看看下列字符串: “08/14/57 46 02/25/59 45 06/05/85 18 03/12/88 16 09/09/90 13“ 这个字符串就是由生日和年龄组成的。如果需要匹配年两而不要生日,就可以把正则
选择排序:从数组的起始位置处开始,把第一个元素与数组中其他元素进行比较。然后,将最小的元素方式在第0个位置上,接着再从第1个位置开始再次进行排序操作。这种操作一直到除最后一个元素外的每一个元素都作为新循环的起始点操作过后才终止。 public void SelectionSort() { int min, temp;
public struct Pqitem { public int priority; public string name; } class CQueue { private ArrayList pqueue; public CQueue() { pqueue
在编写正则表达式的时候,经常会向要向正则表达式添加数量型数据,诸如”精确匹配两次”或者”匹配一次或多次”。利用数量词就可以把这些数据添加到正则表达式里面了。 数量词(+):这个数量词说明正则表达式应该匹配一个或多个紧紧接其前的字符。 string[] words = new string[] { "bad", "boy", "baad", "baaad" ,"bear", "b
来自:http://blog.csdn.net/morewindows/article/details/6678165/归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列
插入排序算法有两层循环。外层循环会啄个遍历数组元素,而内存循环则会把外层循环所选择的元素与该元素在数组内的下一个元素进行比较。如果外层循环选择的元素小于内存循环选择的元素,那么瘦元素都想右移动以便为内存循环元素留出位置。 public void InsertionSort() { int inner, temp;
public int binSearch(int value) { int upperBround, lowerBound, mid; upperBround = arr.Length - 1; lowerBound = 0; while (lowerBound <= upper
虽然从表内第一个节点到最后一个节点的遍历操作是非常简单的,但是反向遍历链表却不是一件容易的事情。如果为Node类添加一个字段来存储指向前一个节点的连接,那么久会使得这个反向操作过程变得容易许多。当向链表插入节点的时候,为了吧数据复制给新的字段会需要执行更多的操作,但是当腰吧节点从表移除的时候就能看到他的改进效果了。 首先需要修改Node类来为累增加一个额外的链接。为了区别两个连接,这个把指
八、树(Tree)树,顾名思义,长得像一棵树,不过通常我们画成一棵倒过来的树,根在上,叶在下。不说那么多了,图一看就懂:当然了,引入了树之后,就不得不引入树的一些概念,这些概念我照样尽量用图,谁会记那么多文字?树这种结构还可以表示成下面这种方式,可见树用来描述包含关系是很不错的,但这种包含关系不得出现交叉重叠区域,否则就不能用树描述了,看图:面试的时候我们经常被考到的是一种叫“二叉树”的结构,二叉
Queue的实现: 就像Stack类的实现所做的一样,Queue类的实现用ArrayList简直是毋庸置疑的。对于这些数据结构类型而言,由于他们都是动态内置的结构,所以ArrayList是极好的实现选择。当需要往队列中插入数据项时,ArrayList会在表中把每一个保留的数据项向前移动一个元素。 class CQueue { private ArrayLis
来自:http://yingyingol.iteye.com/blog/13348911 快速排序介绍:快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地
Stack的实现必须采用一种基本结构来保存数据。因为再新数据项进栈的时候不需要担心调整表的大小,所以选择用arrayList.using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Collecti
数组类测试环境与排序算法using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Data_structure_and_algorithm{ class CArray { pr
一、构造二叉树 二叉树查找树由节点组成,所以需要有个Node类,这个类类似于链表实现中用到的Node类。首先一起来看看Node类的代码。 public class Node { public int Data; public Node Left; public Node Right; public v
二叉树是一种特殊的树。二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。 下面这棵树就是一棵二叉树。         二叉树的使用范围最广,一棵多叉树也可以转化为二叉树,因此我们将着重讲解二叉树。二叉树中还有连两种特殊的二叉树叫做满二叉树和
上一节中我们学习了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据结构它叫做栈。栈限定只能在一端进行插入和删除操作。比如说有一个小桶,小桶的直径只能放一个小球,我们现在向小桶内依次放入2号、1号、3号小球。假如你现在需要拿出2号小球,那就必须先将3号小球拿出,再拿出1号小球,最后才能将2号小球拿出来。在刚才取小球的过程中,我们最先放进去的小球最后才能拿出来,而最后放进去的小球却可以最先拿
msdn中的描述如下:(?= 子表达式)(零宽度正预测先行断言。) 仅当子表达式在此位置的右侧匹配时才继续匹配。例如,w+(?=d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。(?(零宽度正回顾后发断言。) 仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?此构造不会回溯。msdn描述的比较清楚,如:w+(?=ing) 可以匹配以ing结尾的单词(匹配结果不包括ing),(
1.引入线索二叉树 二叉树的遍历实质上是对一个非线性结构实现线性化的过程,使每一个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。但在二叉链表存储结构中,只能找到一个节点的左、右孩子信息,而不能直接得到节点在任一遍历序列中的前驱和后继信息。这些信息只有在遍历的动态过程中才能得到,因此,引入线索二叉树来保存这些从动态过程中得到的信息。 2.建立线索二叉树 为了保
排序与我们日常生活中息息相关,比如,我们要从电话簿中找到某个联系人首先会按照姓氏排序、买火车票会按照出发时间或者时长排序、买东西会按照销量或者好评度排序、查找文件会按照修改时间排序等等。在计算机程序设计中,排序和查找也是最基本的算法,很多其他的算法都是以排序算法为基础,在一般的数据处理或分析中,通常第一步就是进行排序,比如说二分查找,首先要对数据进行排序。在Donald Knuth 的计算机程序设