几种数组排序方法及二维数组作参数传递

来源:互联网 发布:C 阿克曼函数递归算法 编辑:程序博客网 时间:2024/04/28 13:20

定义一个整型数组a[n],下面用五种方法对其从小到大排序。
(1)“冒泡法”
冒泡法大家都较熟悉。其原理为从a[0]开始,依次将其和后面的元素比较,若a[0]>a[i],则交换它们,一直比较到a[n]。同理对a[1],a[2],...a[n-1]处理,即完成排序。下面列出其代码:

void bubble(int *a,int n) /*定义两个参数:数组首地址与数组大小*/
{
int i,j,temp;
for(i=0;i<n-1;i++)
for(j=i+1;j<n;j++) /*注意循环的上下限*/
if(a[i]>a[j]) {
temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}

冒泡法原理简单,但其缺点是交换次数多,效率低。
下面介绍一种源自冒泡法但更有效率的方法“选择法”。

(2)“选择法”
选择法循环过程与冒泡法一致,它还定义了记号k=i,然后依次把a[k]同后面元素比较,若a[k]>a[j],则使k=j.最后看看k=i是否还成立,不成立则交换a[k],a[i],这样就比冒泡法省下许多无用的交换,提高了效率。

void choise(int *a,int n)
{
int i,j,k,temp;
for(i=0;i<n-1;i++) {
k=i; /*给记号赋值*/
for(j=i+1;j<n;j++)
if(a[k]>a[j]) k=j; /*是k总是指向最小元素*/
if(i!=k) { /*当k!=i是才交换,否则a[i]即为最小*/
temp=a[i];
a[i]=a[k];
a[k]=temp;
}
}
}

选择法比冒泡法效率更高,但说到高效率,非“快速法”莫属,现在就让我们来了解它。

(3)“快速法”
快速法定义了三个参数,(数组首地址*a,要排序数组起始元素下标i,要排序数组结束元素下标j). 它首先选一个数组元素(一般为a[(i+j)/2],即中间元素)作为参照,把比它小的元素放到它的左边,比它大的放在右边。然后运用递归,在将它左,右 两个子数组排序,最后完成整个数组的排序。下面分析其代码:

void quick(int *a,int i,int j)
{
int m,n,temp;
int k;
m=i;
n=j;
k=a[(i+j)/2]; /*选取的参照*/
do {
while(a[m]<k&&m<j) m++; /* 从左到右找比k大的元素*/
while(a[n]>k&&n>i) n--; /* 从右到左找比k小的元素*/
if(m<=n) { /*若找到且满足条件,则交换*/
temp=a[m];
a[m]=a[n];
a[n]=temp;
m++;
n--;
}
}while(m<=n);
if(m<j) quick(a,m,j); /*运用递归*/
if(n>i) quick(a,i,n);
}

(4)“插入法”
插入法是一种比较直观的排序方法。它首先把数组头两个元素排好序,再依次把后面的元素插入适当的位置。把数组元素插完也就完成了排序。

void insert(int *a,int n)
{
int i,j,temp;
for(i=1;i<n;i++) {
temp=a[i]; /*temp为要插入的元素*/
j=i-1;
while(j>=0&&temp<a[j]) { /*从a[i-1]开始找比a[i]小的数,同时把数组元素向后移*/
a[j+1]=a[j];
j--;
}
a[j+1]=temp; /*插入*/
}
}

(5)“shell法”
shell法是一个叫 shell 的美国人与1969年发明的。它首先把相距k(k>=1)的那几个元素排好序,再缩小k值(一般取其一半),再排序,直到k=1时完成排序。下面让我们来分析其代码:

void shell(int *a,int n)
{
int i,j,k,x;
k=n/2; /*间距值*/
while(k>=1) {
for(i=k;i<n;i++) {
x=a[i];
j=i-k;
while(j>=0&&x<a[j]) {
a[j+k]=a[j];
j-=k;
}
a[j+k]=x;
}
k/=2; /*缩小间距值*/
}
}

二维数组作为参数传递

先来回顾一下一维数组。一维数组的数组名即为指向该数组的指针,该指针值保存了数组存放在内存中的一块连续区域的起始地址;数组的下标表示了这片内存区域的某存储区相对于起始地址的偏移量。简单来讲就是:指向一维数组的指针,指向数据存放区域的起始位置。

事实上,计算机系统的多维数组其实最终还是以一维数组的形式实现的。就N x M的二维数组来讲,设其数组名为array。指针array指向一个数组,该数组存放的是一系列指针,这些指针分别指向相应的一维数组,而这些数组中存放的才是我们的数据。

array -> [一维数组指针1] -> [ 一维数组,M长]

[一维数组指针2] -> [ 一维数组,M长]

…… ……

[一维数组指针N] -> [ 一维数组,M长]

由此array是第i个指针变量地址,array[j]则表示相对于第i个指针变量偏移j*sizeof(数组类型)。系统通过这种机制访问了该二维数组的第i行,第j列的内容。

有上述可知,指向二维数组的指针其实是指向“指针变量地址”的指针变量。所以在声明指向二维数组的指针时,用 int ** array的形式。

有以下两种方式来对二维数组分配内存:

///// 方法一

#include <stdlib.h> // 必须包含该头文件,里面定义了malloc的实现

int ** array = malloc( N * sizeof(int *) );

for (int k=0;k<N;k++)

array[k] = malloc( M * sizeof(int) );

///// 方法二

#include <stdlib.h>

int ** array = malloc( N * sizeof(int *) );

array[0] = malloc( M * sizeof(int) );

for (int k=1;k<N;k++)

array[k] = array[0]+M*k;

上述两种方法的区别在于:前者在内存中分配的区域有可能是不连续的;而后者则在内存中的一片连续区域为该数组分配空间。

我们还可以通过一维数组模拟二维数组。在这中间要进行下标转换。如对于模拟的NxM数组,访问其第i行,第j列元素时,在一维数组中对应的位置是i*M+j。当然为了更简捷,我们可以把这个数组下标转换过程定义为一个宏,交由编译系统来处理。

#define Arr2 ( array_name, row,col ) array_name[row*M+col]

定义该宏后,访问Arr2( array, i, j)等价于访问 array[i*M+j]。



【以下转帖】
----------------------------------------------------------------------------------------------
但一般传递二维数组的基本规则好像是这样的:可以用二维数组名作为实参或者形参,在被调用函数中对形参数组定义时可以可以指定所有维数的大小,也可以省略第一维的大小说明。如:

    void Func(int array[3][10]);
    void Func(int array[][10]);

二者都是合法而且等价,但是不能把第二维或者更高维的大小省略,如下面的定义是不合法的:
    void Func(int array[][]);

将二维数组当作参数的时候,必须指明所有维数大小或者省略第一维的,但是不能省略第二维或者更高维的大小,这是由编译器原理限制的。在学编译原理这么课程的时候知道编译器是这样处理数组的:
对于数组 int p[m][n]; 如果要取p[i][j]的值(i>=0 && i<m && 0<=j && j < n),编译器是这样寻址的,它的地址为:
     p + i*n + j;

从以上可以看出,如果我们省略了第二维或者更高维的大小,编译器将不知道如何正确的寻址。但是我们在编写程序的时候却需要用到各个维数都不固定的二 维数组作为参数,这就难办了,编译器不能识别阿,怎么办呢?不要着急,编译器虽然不能识别,但是我们完全可以不把它当作一个二维数组,而是把它当作一个普 通的指针,再另外加上两个参数指明各个维数,然后我们为二维数组手工寻址,这样就达到了将二维数组作为函数的参数传递的目的,根据这个思想,我们可以把维 数固定的参数变为维数随即的参数,例如:

    void Func(int array[3][10]);
    void Func(int array[][10]);
变为:
    void Func(int **array, int m, int n);

在转变后的函数中,array[i][j]这样的式子是不对的(不信,大家可以试一下),因为编译器不能正确的为它寻址,所以我们需要模仿编译器的行为把array[i][j]这样的式子手工转变为

    *((int*)array + n*i + j);

在调用这样的函数的时候,需要注意一下,如下面的例子:
    int a[3][3] =
    {
      {1, 1, 1},
      {2, 2, 2},
      {3, 3, 3}
    };
    Func(a, 3, 3);

根据不同编译器不同的设置,可能出现warning 或者error,可以进行强制转换如下调用:  
    Func((int**)a, 3, 3);
----------------------------------------------------------------------------------------------

需要(int**)的强制转换,是因为二维数组和二级指针是不同的,a实质上是一个int (*a)[3],它是一个数组指针,即a[0]是第一维数组的首个元素的地址,a[1]是第二维数组的首个元素的地址,a[2]是第三维数组的首个元素的 地址,与int**是不同的类型;如果转为int**,就失去了像数组指针那样a + i = a + i*3的效果了
而如果又定义一个char *p[3],它是一个一维的指针数组,此时p是指向了一个指针,而不是数组。那么这时如果定义char **q = p,就是可以的,而且可以通过q[0],q[1]来访问字符串。

数组和指针这种东西真是太繁琐复杂了,个人愚见,在C++里就尽量使用STL,并且可以用模板的非类型形参来解决这种灵活处理不固定行列数矩阵的函数,Effective C++里面应该有介绍,并且有对这种模板的优化。

 

二维数组的动态分配(new)、初始化(memset)和撤销(delete)

一维数组的动态分配,初始化和撤销都好说,几乎每一本C++教材都会做出详细的说明。具体如下:

动态分配(例如分配10个单元的): int *array=new int [10];

初始化:memset(array,0,sizeof(array));       (也可以利用一个for循环对其赋值初始化)

撤销:delete [] array;

下面来说二维数组的。

二维数组(n行m列)利用new来进行动态分配实际上相当于对n个m元数组进行动态分配,只不过我们不能一味的按照动态分配一维数组的方法来这项操作。MSVC目前还没有这般的人性化,具体应该这样做:

int **array;
array=new int *[10];
for(int i=0;i<10;i++)
         array[i]=new int [5];


   上面的操作完成了一个10行5列的二维数组array[10][5]的动态分配,可以看到我们先动态分配了一个10单元的数组的指针的指针的首地址**array,然后再对其每个首地址进行遍历,同时完成一个5单元的数组的动态分分配,并把首地址给*array[i],从而最终完成了二维数组array[10][5]的动态分配。我们可以依此类推得到三维以至多维的数组的动态分配方法。

二维数组的初始化:如果把一维数组初始化办法照搬过来就会发现对于动态分配的二维数组并不适用。这就要理解到memset这个函数三个参数的含义。MSDN对memset的描述如下:

memset

Sets buffers to a specified character.

void *memset( void *dest, int c, size_t count );

可见memset只能作用于一个一维数组void *dest,因此最好的办法就是和二维数组的动态分配结合起来,new一个,memset一个。具体写法如下:

int **array;
array=new int *[10];
for(int i=0;i<10;i++)

{
         array[i]=new int [5];

        memset(array,0,5*sizeof(int));

}