数据结构基础温故而知新(二)——数组
数组可以看成是一种特殊的线性表,是线性表的推广,其特点是数据元素仍然是一个表,即线性表中数据元素本身也是一个线性表
数组的定义:
数组是定长线性表在维数上的扩张,即线性表中的元素又是一个线性表,n维数组是一种“同构”的数据结构,其中每个数据元素类型相同,结构一致。、
设有n维数组A[b1,b2,…,bn],其每一维的下界都为1,bi是第i维的上界。从数据结构的逻辑关系角度来看,A中的每个元素A[j1,j2, …,jn](1≤ji≤bi)都被n个关系所约束。在每个关系中,除第一个和最后一个元素外,其余元素都只有一个直接后继和一个直接前驱。因此就单个关系而言,仍是线性的。
以二维数组A[m,n]为例,可以把它看成是一个定长的线性表,它的每个元素也是一个定长线性表。
A可看成一个行向量形式的线性表:
Am,n=[[a11a12…a1n], [a21a22…a2n], …, [am1am2…amn]];
或列向量形式的线性表:
Am,n=[[a11a21…am1], [a12a22…am2], …, [a1na2n…amn]];
数组结构的特点如下:
(1) 数据元素数目固定,一旦定义了一个数组结构,就不再有元素的增减变化。
(2) 数据元素具有相同的类型。
(3) 数据元素的下标关系具有上下界得约束且下标有序。
数组的两个基本运算
(1) 给定一组下标,存取相应的数据元素。
(2) 给定一组下标,修改相应的数据元素中某个数据项的值。
几乎所有的高级程序设计语言都提供了数组类型。实际上,在程序语言中把数组看成是具有共同名字的同一类型多个变量的集合。
数组的顺序存储
数组一般不作插入和删除运算,一旦定义了数组,则结构中的数据元素个数和元素之间的关系就不再发生变动,因此数组适合于采用顺序存储结构。
由于计算机的内存结构是一维线性的,因此存储多维数组时必须按某种方式进行降维处理,即将数组元素排成一个线性序列,这就产生了次序约定问题。因为多维数组是由较低一维的数组来定义的,依次类推,通过这种递推关系将多维数组的数据元素排成一个线性序列。
对于数组,一旦确定了其维度和各维的长度,便可为它分配存储空间。反之,只要给出一组下标便可求得相应数组元素的存储位置,即在数据的顺序存储结构中,数据元素的位置是其下标的线性函数。
二维数组的存储结构可分为以行为主序的两种方法(如下图)
设每个数据元素占用L个单元,m,n为数组的行数和列数,Loc(a11)表示元素a11的地址,那么以行为主序优先存储的地址计算公式为:
Loc(aij)=Loc(a11)+((i-1)×n+(j-1))×L
同理,以列为主序优先存储的地址计算公式为:
Loc(aij)=Loc(a11)+((j-1)×m+(i-1))×L
推广至多维数组,按下标顺序存储时,先排最右的下标,从右向左直到最左下标,而逆下标顺序则正好相反。
C#中的数组
C#支持一维数组、多维数组及交错数组(数组的数组)。所有的数组类型都隐含继承自System.Array。Array是一个抽象类,本身又继承自System.Object。所以,数组总是在托管堆上分配空间,是引用类型。任何数组变量包含的是一个指向数组的引用,而非数组本身。当数组中的元素的值类型时,该类型所需的内存空间也作为数组的一部分而分配;当数组的元素是引用类型时,数组包含是只是引用。Array还继承了ICloneable、IList、ICollection、IEnumerable等接口。
C#中的数组一般是0基数组(最小索引为0),这是为了和其它语言共享代码。C#也支持非0基数组。C#除了能创建静态数组外,还可以创建动态数组,这通过使用Array的静态方法CreateInstance方法来实现。
Array中的方法有许多。以下列出了Array类型中常用的方法,并对每个方法给出了注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | public abstract class Array : ICloneable, IList, ICollection, IEnumerable { //判断Array是否具有固定大小。 public bool IsFixedSize { get ;} //获取Array元素的个数。 public int Length { get ;} //获取Array的秩(维数)。 public int Rank { get ; } //实现的IComparable接口,在.Array中搜索特定元素。 public static int BinarySearch(Array array, object value); //实现的IComparable<T>泛型接口,在Array中搜索特定元素。 public static int BinarySearch<T>(T[] array, T value); //实现IComparable接口,在Array的某个范围中搜索值。 public static int BinarySearch(Array array, int index, int length, object value); //实现的IComparable<T>泛型接口,在Array中搜索值。 public static int BinarySearch<T>(T[] array, int index, int length, T value); //Array设置为零、false 或null,具体取决于元素类型。 public static void Clear(Array array, int index, int length); //System.Array的浅表副本。 public object Clone(); //从第一个元素开始复制Array 中的一系列元素 //到另一Array中(从第一个元素开始)。 public static void Copy(Array sourceArray, Array destinationArray, int length); //将一维Array的所有元素复制到指定的一维Array中。 public void CopyTo(Array array, int index); //创建使用从零开始的索引、具有指定Type和维长的多维Array。 public static Array CreateInstance(Type elementType, params int [] lengths); //返回ArrayIEnumerator。 public IEnumerator GetEnumerator(); //获取Array指定维中的元素数。 public int GetLength( int dimension); //获取一维Array中指定位置的值。 public object GetValue( int index); //返回整个一维Array中第一个匹配项的索引。 public static int IndexOf(Array array, object value); //返回整个.Array中第一个匹配项的索引。 public static int IndexOf<T>(T[] array, T value); //返回整个一维Array中最后一个匹配项的索引。 public static int LastIndexOf(Array array, object value); //反转整个一维Array中元素的顺序。 public static void Reverse(Array array); //设置给一维Array中指定位置的元素。 public void SetValue( object value, int index); //对整个一维Array中的元素进行排序。 public static void Sort(Array array); } |
数组是基本的数据结构,因为在所有计算上,它们与内存系统存在直接的对应关系。为了用机器语言从内存中读取内容,就要提供相应的地址。因此,我们可以将整个计算机内存看做一个数组,让内存地址与数组索引对应。大部分计算机语言处理器包含数组的程序翻译成直接访问内存的高效机器语言程序。保守的假定是,数组访问(a[i])只翻译成几条机器指令。
厄拉多塞筛
这个程序的目标是,若i为素数,则设置a[i]为1;若不是素数,则设置为0.首先,将所有数组元素设置为1,表示没有已知的非素数。然后将已知为非素数(即为已知素数的倍数)的索引对应的数组元素设置为0.如果将所有较小素数的倍数设置为0之后,a[i]仍然保持1,则可判断它是所找的素数。
C语言描述
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include "stdafx.h" #include "malloc.h" #define N 10000 void _tmain( int argc, _TCHAR* argv[]) { int i,j,a[N]; for (i=2;i<N;i++) a[i]=1; for (i=2;i<N;i++) if (a[i]) for (j=i;j<N/i;j++) a[i*j]=0; for (i=2;i<N;i++) if (a[i]) printf( "%4d" ,i); printf( "\n" ); } |
C#语言
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | static int N = 10000; static int RAND_MAX = 1000; static void Main( string [] args) { int i, j; int [] a = new int [N]; for (i = 2; i < N; i++) { a[i] = 1; } for (i = 2; i < N; i++) { if (a[i] == 1) { for (j = i; j < N / i; j++) { a[i * j] = 0; } } } for (i = 2; i < N; i++) { if (a[i] == 1) { Console.Write( string .Format( "{0} " , i.ToString())); } } Console.ReadKey(); } |
因为程序使用一个数组来包含最简单元素类型,0和1两个值,如果我们使用位的数组,而不是使用整数的数组,则可获得更高的空间有效性。而且,如果N庞大,一些编程环境可能要求数组为全局,或者可以动态分配它。如下:
1 2 3 4 | long int i, j, N = atoi(argv[1]); int *a = malloc(N* sizeof ( int )); if (a == NULL) { printf( "Insufficient memory.\n" ); return ; } |
它的运行时间与下式成比例:
N+N/2+N/3+N/5+N/7+N/11+…
数组不仅深刻地反映在大部分计算机上访问内存中数据的底层机制,而且还有着广泛的用途,因为它们与组织应用中数据的自然方法直接吻合。例如,数组也与向量直接对应,向量是对象索引表(indexed list)的数学术语。
以下程序模拟了一个伯努利试验(Bernoulli trial)序列,这是源自概率论的一个熟悉的抽象概念。如果我们抛掷硬币N次,K次看到正面头像的概率为:
硬币抛掷模拟
如果抛掷硬币N次,看到头像的期望值是N/2次,但实际值也可能是0~N次,在程序中进行M次试验,M和N都从命名行获取。它使用一个数组f来跟踪出现“i次头像”的概率,其中0≤j≤N。然后打印试验结果的柱状图,每出现10次用1个星号表示。
该程序的运算基础(即用一个计算值来索引组)是许多计算进程的效率关键所在
C语言
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include "stdafx.h" #include "stdlib.h" #define RAND_MAX 1000 int heads() { return rand()<RAND_MAX/2; } void main( int argc, char * argv[]) { int i,j,cnt; int N=atoi(argv[1]),M=atoi(argv[2]); int *f=malloc((N+1)* sizeof ( int )); for (j=0;j<=N;j++) f[j]=0; for (i=0;i<M;i++,f[cnt]++) for (cnt=0,j=0;j<=N;j++) if (heads()) cnt++; for (j=0;j<=N;j++) { printf( "%2d" ,j); for (i=0;i<f[j];i+=10) printf( "*" ); printf( "\n" ); } } |
C#语言
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | static Random mm = new Random(); static bool heads() { int n = mm.Next(1000); return n < RAND_MAX / 2; } private static void Array2( int N, int M) { int i, j, cnt; int [] f = new int [N + 1]; for (j = 0; j <= N; j++) f[j] = 0; for (i = 0; i < M; i++, f[cnt]++) { for (cnt = 0, j = 0; j <= N; j++) if (heads()) cnt++; } for (j = 0; j <= N; j++) { Console.Write(j); for (i = 0; i < f[j]; i += 10) Console.Write( "*" ); Console.WriteLine(); } } |
这种近似被称之为正态近似(normal approximation),其曲线为我们熟悉的铃形。计算中的重点是,使用数值作为数值的索引来计算出现的频率。数组支持这类运算的能力是它的优点之一。
从某种意义上讲,当我们使用计算值来访问大小为N的数组时,就是只用一个运算来考虑N中概率。它对效率的提升非常可观。
【参考资料】
陈广 《数据结构(c#语言描述)》 北京大学出版社
美国 Robert.Sedgewick 周良忠 译 《第1卷:基础、数据结构、排序和搜索》 人民邮电出版社
严蔚敏 《数据结构c语言版》 清华大学出版社
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步