6.1 6.2 数组、稀疏矩阵

来源:互联网 发布:彩虹timez知乎 编辑:程序博客网 时间:2024/05/16 11:03

数组

数组的一般定义

数组是一种由相同数据类型构成的序列。数组的本质就是一个线性表。对于一维数组,该线性表中的元素类型就是需要存放的类型ElementType,对于多维数组,其维度d2,则可以认为线性表的元素类型是d1维数组。这样来看,数组也是一种递归的定义。
对于数组来说,通常只有以下两种操作:读取和写入。这两种操作需要一个输入向量来表示下标(即用来定位)。
数组中最常见的是一维数组和二维数组,分别可以对应线性代数中的向量(vector)和矩阵(matrix)。

在教材采用的C/C++语言中,可以很自然的使用形如int A[10]这样的语句来定义一个数组。对于这样定义的数组,其长度是定长的,不允许在运行时改变。这样的数组定义也只能出现在栈内存区或者全局内存区。C99标准支持int n = 10; int A[n];这样的用法,但是C++11标准不支持(因为STL容器是一个更好的替代)。对于堆内存的数组,可以使用指针,如int * A = (int *)malloc(sizeof(int) * 10);来创建一个数组(指针指向数组的首地址);实际上,利用这种方式包装好一个容器,例如STL的std::vector类,是更方便、更安全、更有效率的。顺带一提,STL的std::vector类不支持直接存放二维(多维)数组,只能使用vector<vector<int>>这种类似的方式进行存放。这样在使用push_back方法将新的一维数组压入容器,可能存在反复执行构造函数的开销;对于C++11可以使用右值语义(R-Value)和std::move以尽量减小这个问题带来的效率损失。

数组的存储结构

数组是一种线性表,因此需要连续的存放在存储器的相应位置。对于一维数组的存储结构,其实比较简单。在C/C++语言中,只需要一个指针指向数组的首地址,就能够表示一个数组。当然,必须要有一个标记,来指示数组的结束(例如使用length字段标识数组的长度,亦或C风格字符串的\0结束符)。
因此,对于一个一维数组A,其元素A[i](按照C/C++的习惯i从0开始)的实际地址p=A+ki
而对于一个二维数组,其输入是一个二元组,因此需要设法构造出一种映射,将其映射在存储器的连续的相应位置。常用的方法有行优先列优先
对于一个二维数组A[m][n],行优先是指,A[i]取出的元素,是表示第i行的一个一维数组;而列优先A[i]表示的是第i列的一个一维数组。因此不难得出下面的计算公式:对于A[m][n]A[i][j]元素的位置
行优先:p=A+k(ni+j)i个满行,紧接着的一行的第j个元素)
列优先:p=A+k(mi+j)j个满列,紧接着的一列的第j个元素)
C/C++的编译器默认按照行优先存放。

最后需要提及的是,并不是所有语言的数组都是使用一串连续存储空间实现的。之前提及过的PHP的数组,是使用的哈希表来处理键值对(字符串类型的键也可以作为数组的下标)。

数组与特殊矩阵

矩阵可以使用二维数组进行存储。对于特殊形式的矩阵(对称阵、上下三角阵),只需要存储一半的元素就可以了。
常见的存储映射是,将二维降维,映射在一维数组上。以下三角阵为例,对于Amn,用一个向量B=(b1,b2,...bmn2)来表示,则b1=a11b2=a21b3=a22,以此类推,则aij=bi(i1)2+j(假设i,j都从1开始)。总计可以节省近一半的空间。
总之,特殊矩阵的存储,在于根据问题的情况找到一个合适的映射(能在O(1)时间内计算出的),将二维降低到一维。

稀疏矩阵

首先给出稀疏矩阵的一个定义:一个m×n的矩阵,当非零元个数tmn,这样的矩阵称为稀疏矩阵(sparse matrix)。
稀疏矩阵的非零元分布具有随机性,因此不能简单作之前的映射。因此可以用键值对的线性表,来描述一个稀疏矩阵。
这里的键值对可以使用std::pair<std::pair<int, int>, ElementType>来表示,也可以使用C++11的std::tuple<int, int, ElementType>这个三元组类来表示。这里我更倾向使用键值对的表示,前一个pair作为二元输入向量,而外层的pair的第二个元素作为下标为输入向量的位置的值。
因此可以用std::vector<std::pair<std::pair<int, int>, ElementType>>来表示一个稀疏矩阵。

初始情况下,这个线性表容器是空的,表示这个矩阵是零矩阵O(不含任何非零元)。对于一个非零的稀疏矩阵表示,修改(i,j) 的元素值为v(v0),即((i,j),v),则首先需要判断(i,j)是否已经为非零元,即遍历一次容器,查找一下是否存在键为(i,j)的元素。如果是,直接修改;否则,需要在合适位置将((i,j),v)放进去(为了维护有序性,按照下标的输入向量排序,便于查找)。

对于一个稀疏矩阵而言,其赋值操作的最坏运行时间取决于非零元的个数t而并非矩阵的实际元素个数mn。而稀疏矩阵本身就是为了避免空间浪费的,属于时间换空间。此时无论是O(lgn)的红黑树的std::map还是O(1)的哈希表的std::unordered_map都是会造成大量空间浪费,违背了稀疏矩阵的初衷。

一道不错的实验题

已知一个二维数组A[m][n],其每一行的元素都递增,且每一行的第一个元素都大于上一行的任意元素。求这个数组中的指定元素x

最原始的办法是进行朴素搜索,需要O(mn)的时间。如果要利用有序性,可以采用二分法。对于每一行元素都递增,可以对行使用二分。这样就有了O(mlgn)的时间。由于每一行的第一个元素都大于上一行的所有元素,因此可以先二分确定元素可能处在的行,然后再对行进行二分。这样的时间复杂度为O(lgm+lgn)
说起来很简单,但是编码还是很有难度的。尤其是第一层二分时,当元素不能在这一行时,必然满足小于第一个元素或者大于最后一个元素。除此之外,lower_bound返回的是不小于指定值的最小下标。因此需要判断返回的值是否等于需要查找的值。

//Not Found Constant Defineconst std::pair<int, int> not_found(-1, -1);/*** @brief 查找二维数组中指定元素的位置* @param std::vector<std::vector<int>> & matrix 输入矩阵* @param int val 待查找元素的值* @return std::pair<int, int> 返回二元组表示的元素的位置*/std::pair<int, int> matrix_find(std::vector<std::vector<int> > & matrix, int val) {    //利用有序性二分查找,M*N矩阵,平均时间复杂度O(logM+logN)    int cbegin, cend, cmid;    cbegin = 0;    cend = matrix.size() - 1;   //二分查找的end初始设置为最大的能取得值    while (cbegin < cend) {        cmid = (cbegin + cend) / 2;        if (val > matrix[cmid].back())            cbegin = cmid + 1;        else if (val < matrix[cmid].front())            cend = cmid - 1;        else            break;    }    //定位元素可能在的行    int row = (cbegin == cend) ? cend : cmid;    //利用lower_bound查找所在位置    std::vector<int>::iterator iter = lower_bound(matrix[row].begin(), matrix[row].end(), val);    if (iter != matrix[row].end() && *iter == val)        return make_pair(row, iter - matrix[row].begin());    else        return not_found;}
原创粉丝点击