「数据结构与算法」什么是数组,如何用C实现一个简单的动态数组

如果你用过R语言,那么一定用过向量,如果用Python,那么一定用过列表。那么问题来了,这两类数据结构有什么区别呢?

为什么Python的列表,支持存放不同类型的数据,而R语言的向量只能放同一个类型的数据呢?还有,为什么R语言的向量化运算函数(如sum, nchar)速度会显著性高于R的循环呢?

对于下面的R代码,那种循环更好呢?

# loop 1
a <- c()
for (i in 1:1000){
   a <- c(a,i)
}
# loop 2
a <- vector(length=1000)
for (i in 1:1000){
    a[i] = i
}

要想解答这些问题,就必须得学习一下数组。数组是一种线性表数据结构,他在一段连续内存空间中存储相同大小的元素。这里有两个关键点,第一是线性表,也就是说数组里面的元素只有前后关系,同样属于线性表数据结构的还有链表、队列和栈,与之相反的是非线性表数据结构,例如树和图。

线性表和非线性表

第二是连续内存,且里面的元素大小相同,这样子当知道数组的第一个元素的内存位置时,就可以迅速计算出第n个元素的内存地址,并获取该地址的存储内容。

随机访问

有利就有弊。由于数组要占用一组连续的内存空间,当你要存储的数据要占据非常大的空间时,就会面临内存中找不到位置的尴尬情形。此外,为了保证数据存储的连续性,那么在你插入和删除数据的时候都要进行额外的数据移动操作,这些操作的时间复杂度是0(n)。如果数据已经满了,那么加入新的数据时就需要在内存中申请新的空间,同时将现有数据迁移过去。

插入操作

尽管如此,数组依旧是最常用的数据结构,毕竟数组和CPU和缓存机制非常契合,在数据处理上效率较高。

在C语言中,数组的声明方式有如下方法:

float averages[200]; //在内存中预留200个位置
char word[] = { 'H', 'e', 'l', 'l', 'o', '!'}; // 让C自动确定数组的大小
char *words[] = {"hello", "world"}; 字符串数组

此外,指针与数组的关系十分密切,一般能用数组下标实现的操作,都能用数组完成。过去的时候,指针操作的速度会快于数组下标操作,但是随着编译器的优化,基本上两者的性能持平了。考虑指针实现的程序理解比较困难,因此更推荐用数组。示例:

int a[10]; //声明一个长度为10的存放整型的数组
int *pa;  //声明一个指向整型的指针
pa = &a[0]; // 将数组a的起始地址赋值给指针
//等价于 pa = a;

那么a[i] 等价于 *(pa + i ) , 无论数组a中元素的类型或数组长度是什么,该结论始终成立。 ”指针加1“就意味着pa + 1 指向pa所指向对象的下一个对象。简而言之,一个通过数组和下标实现的表达式可等价地通过指针和偏移量实现

但是,指针和数组还是有区别的,指针是一个变量,数组名不是变量。在函数定义中,形式参数char s[];char *s 是等价的,这是因为把数组名传递给函数时,实际传递的时该数组的第一个元素的地址。

R语言的向量和Python的列表还不是普通的数组,因为他们的大小可变,同样特点的还有C++的标准库类型vector, 还有Java的ArrayList和Vector类,都支持动态进行扩容。举个C++的例子

#include <vector>
#include <iostream>

using std::cin;
using std::vector;

string word;
vector<string>  text;
while (cin >> word){
  text
}

当然C语言本身并没有这个功能,所以我就尝试着自己写了一个非常简单的实现,依旧分为两个头文件和C文件。

darray.h如下:

#ifndef _DARRAY_H                                           
#define _DARRAY_H                                           
                                                            
typedef unsigned int position;                              
                                                            
/* define the data struct */                                
typedef struct _cell cell;                                  
typedef struct _darray darray;                              
                                                            
/* define the manipulate function of darray */              
                                                            
darray *dCreate(char *strings[], int ssize);                
darray *dInsert(darray *d, position pos, char *string);     
darray *dExpand(darray *d, int ssize);                      
void dDestroy(darray *d);                                   
void dRemove(darray *d, position pos);                      
void dPrint(darray *d);                                     
                                                            
#endif                                                      

darray.c:

#include <stdio.h>
#include <stdlib.h>
#include "dbg.h"
#include "darray.h"

// 用deleted标注删除,不做实际的搬移操作
enum status {empty, deleted, legitimate };

typedef struct _cell {
    enum status info;
    char *string ;
} cell;

typedef struct _darray{
    int size;
    int load;
    cell *cells;
} darray;

// 根据初始大小申请内存
darray *dCreate(char *strings[], int ssize)
{
    darray *d;
    d = malloc(sizeof(darray));
    d->size = 2 * ssize;
    d->load = 0;
    d->cells = calloc(d->size, sizeof(cell));
    
    // initialize the array
    int i;
    for (i = 0; i < ssize; i++){
        d->cells[i].info = legitimate;
        d->cells[i].string = strings[i];    
        d->load ++;
    }
    debug("i shoule be %d", i);
    for (; i < d->size; i++){
        d->cells[i].info = empty;   
    }
    return d;
}
//进行扩容
darray *dExpand(darray *d, int ssize)
{
    darray *newArray;
    newArray = malloc(sizeof(darray));
    newArray->size = 2 * ssize;
    newArray->cells = calloc(newArray->size, sizeof(cell));
    newArray->load = 0;
    int i;
    int j = 0;
        //复制元素
    for (i = 0; i < d->size; i++){
        if( d->cells[i].info == legitimate){
            newArray->cells[j].string = d->cells[i].string; 
            debug("new arrys cell %d is %s", j, newArray->cells[j].string);
            newArray->cells[j].info = legitimate;
            newArray->load++ ;
            j++ ;
        }
    }
    for (; j < newArray->size; j++){
        newArray->cells[j].info = empty;
    }
        //释放原来的内存
    dDestroy(d);
    return newArray;
}

//插入操作
darray *dInsert(darray *d, position pos, char *string)
{
    if (d->load + 1 > d->size ){
        d = dExpand(d, d->size);
    }

    if (pos > d->size ){
        d = dExpand(d, pos);
    }
    debug("size of darray is %d", d->size);
    if ( d->cells[pos].info == deleted ||
            d->cells[pos].info == empty){
        d->cells[pos].info = legitimate;
        d->cells[pos].string = string;
    } else{
        int i = d->size;
        d->cells[i].info = legitimate;
        for (; i > pos; i--){
            d->cells[i].string = d->cells[i-1].string;
        }
        d->cells[pos].string = string;
    }
    return d;

}
//删除操作
void dRemove(darray *d, position pos){
    d->cells[pos].info = deleted;
}
//输出元素
void dPrint(darray *d)
{
    int i = 0;
    for (i = 0; i < d->size; i++){
        if ( d->cells[i].info == legitimate)
            printf("%d \t %s \n", i, d->cells[i].string);
    }
    putchar('\n');
}

void dDestroy(darray *d)
{
    free(d->cells);
    free(d);
}

int main(int argc, char *argv[])
{
    darray *test;
    char *input[] = {"hello", "my", "world","!"} ;
    test = dCreate(input, 4);
    dPrint(test);
    char *h = "hello";
    test = dInsert(test, 10, h);
    dPrint(test);
    dRemove(test,1);
    dPrint(test);
    dDestroy(test);
    return 0;

}

目前代码还存在一些问题,因为我只是将元素标记了删除,那么后续删除同一个位置时需要向后移动才行。同样插入操作也会存在bug。但是能这样子写代码对之前只能hello world的我已经是很大进步了。

解答开篇: Python的列表中存放的是元素的引用,并非元素本身,因此可以放任意类型的数据,其实和R语言的list更加对应。而R语言的向量则更加接近数组结构。

当你使用a <c()的结果是在内存中申请了一块固定大小的空间。之后每次的a<- c(a, i)的效果就是在内存不断申请新空间,加入元素,因此时间消耗会很明显。所以,事先声明足够大的空间然后进行赋值操作才会比较经济。

当我看C++ Primer(第五版)的vector一节时(P91页),里面提到,C++里面反而不应该像C,Java那样事先声明元素数目。事先声明元素大小反而会降低效率。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,930评论 2 89
  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,792评论 0 13
  • 指针是C语言中广泛使用的一种数据类型。 运用指针编程是C语言最主要的风格之一。利用指针变量可以表示各种数据结构; ...
    朱森阅读 3,442评论 3 44
  • 舟山归来不看海 海客谈瀛洲,烟波微茫信难求。南海之路,确实不易。多年前就想仿仙山,然一拖再...
    行走布衣阅读 563评论 0 1
  • 有人嘉许你 那只是对方遇见了美丽 属于他的嘉许 其实跟你没关系 所以 你可以看见他的嘉许 并回以嘉许 有人嫉妒你 ...
    登泓听香阅读 158评论 0 0