小肥柴慢慢手写数据结构(C篇)(1-3 线性表 ArrayList 严版教材实现浅析)

小肥柴慢慢手写数据结构(C篇)(1-3 线性表 ArrayList 严版教材实现浅析)

目录

准备工作

  1. 复习指针:
    (1)C语言重点——指针篇(一篇让你完全搞懂指针)
    其中有一段评论我觉得是学习的真谛,直接debug看地址,看数据:
    在这里插入图片描述
    (2)从5个维度来看C语言指针(指针就是个纸老虎)
    其中有一个评论我很赞同:“指针自身并没有数据类型一说”
    (3)c语言函数指针的理解与使用(函数指针的本质,很实用)

  2. 自己预先拿到严版官方的全套代码,因为我们已经实现过一遍了,不着急马上再写一遍,可以慢慢学(chao)习(xi),主要是通过看强化对基本概念、知识点和技巧的理解,最后尝试从linux源码中找类似的代码看看实用起来是什么样子。

1-9 对比代码,理顺学习思路和基本语法

  1. 先看struct
    (1)self版
#define DEFAULT_CAPACITY (20)
struct ArrayList {
	ElementType *data;
	int len;
	int capacity;
};

typedef struct ArrayList *PtrArrayList;
typedef PtrArrayList List;

(2)严版

 // c2-1.h 线性表的动态分配顺序存储结构
#define LIST_INIT_SIZE 10 // 线性表存储空间的初始分配量
#define LISTINCREMENT 2 // 线性表存储空间的分配增量
struct SqList
{
  ElemType *elem; // 存储空间基址
  int length; // 当前长度
  int listsize; // 当前分配的存储容量(以sizeof(ElemType)为单位)
};

1)代码形式基本上一样,只是严版中每次扩容LISTINCREMENT=2个单位,步子小;反观self版本步子大,直接*2,所以这里需要折中优化,建议同学们尝试寻找下比较优秀的步进策略。
2)严版代码宏定义洒脱,不用括号护体,self相对规范些。

  1. include头文件和各类状态码
    (1)self版
    ArrayList.h
#ifndef _Array_List_h
#define _Array_List_h
#define OK (0)
#define ERROR (-1)
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ArrayList.h"

(2)严版
c1.h

#include<string.h>
#include<ctype.h>
#include<malloc.h> // malloc()等
#include<limits.h> // INT_MAX等
#include<stdio.h> // EOF(=^Z或F6),NULL
#include<stdlib.h> // atoi()
#include<io.h> // eof()
#include<math.h> // floor(),ceil(),abs()
#include<process.h> // exit()
#include<iostream.h> // cout,cin
// 函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
// #define OVERFLOW -2 因为在math.h中已定义OVERFLOW的值为3,故去掉此行
typedef int Status; // Status是函数的类型,其值是函数结果状态代码,如OK等
typedef int Boolean; // Boolean是布尔类型,其值是TRUE或FALSE

1)self中有防止重复引用的宏定义,这点比严版要规范。
2)严版对状态的定义更丰富,本身使用了大量的lib。

  1. 功能上,严版提供的API更丰富
    (1)基础功能
Status InitList(SqList &L) //构造一个空表
Status DestroyList(SqList &L) //销毁
Status ClearList(SqList &L) //重置空表
Status ListEmpty(SqList L)  //判空
int ListLength(SqList L) //返回L中元素个数
Status GetElem(SqList L,int i,ElemType &e) //用e返回L中第i个元素
int LocateElem(SqList L,ElemType e,Status(*compare)(ElemType,ElemType)) //返回第1个与e满足关系compare()的元素的位序
Status PriorElem(SqList L,ElemType cur_e,ElemType &pre_e) //前置元素
Status NextElem(SqList L,ElemType cur_e,ElemType &next_e) //后置元素
Status ListInsert(SqList &L,int i,ElemType e) //插入,类似于self的add
Status ListDelete(SqList &L,int i,ElemType &e) //删除
Status ListTraverse(SqList L,void(*vi)(ElemType&)) //打印

(2)拓展功能

void InsertAscend(SqList &L,ElemType e) //按非降序插入新的数据元素e
void InsertDescend(SqList &L,ElemType e) //按非升序插入新的数据元素e
Status HeadInsert(SqList &L,ElemType e) //头插
Status EndInsert(SqList &L,ElemType e) //尾插
Status DeleteFirst(SqList &L,ElemType &e) //删除第一个目标元素e
Status DeleteTail(SqList &L,ElemType &e) //删除最后一个目标元素e
Status DeleteElem(SqList &L,ElemType e) //删除元素e
Status ReplaceElem(SqList L,int i,ElemType e) //设置
Status CreatAscend(SqList &L,int n) //按非降序建立n个元素的线性表
Status CreatDescend(SqList &L,int n) //按非升序建立n个元素的线性表
Status equal(ElemType c1,ElemType c2) //判断是否相等
void Union(SqList &La,SqList Lb) //求并集
void MergeList(SqList La,SqList Lb,SqList &Lc) //归并La、Lb到新的Lc

需要注意 &L的含义:直接给地址,既是入参也是出参!

  1. 选几个函数做案例分析
    (1)ListInsert( )
Status ListInsert(SqList &L,int i,ElemType e) // 算法2.4
{ // 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)+1
 // 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
  ElemType *newbase,*q,*p;
  if(i<1||i>L.length+1) // i值不合法
  return ERROR;
 if(L.length>=L.listsize) // 当前存储空间已满,增加分配
 {
   if(!(newbase=(ElemType *)realloc(L.elem,(L.listsize+LISTINCREMENT)*sizeof(ElemType))))
     exit(OVERFLOW); // 存储分配失败
   L.elem=newbase; // 新基址
   L.listsize+=LISTINCREMENT; // 增加存储容量
 }
 q=L.elem+i-1; // q为插入位置
 for(p=L.elem+L.length-1;p>=q;--p) // 插入位置及之后的元素右移
   *(p+1)=*p;
 *q=e; // 插入e
 ++L.length; // 表长增1
 return OK;
}

细节区别:
1)初始条件都是默认表L已经存在,即L!=NULL
2)使用!判断内存分配结果,常用前++/后++,让代码看起来更加紧凑
3)其实原版书中的代码实现比配套代码文件中的实现晦涩一些

q=L.elem+i-1; // q为插入位置
for(p=L.elem+L.length-1;p>=q;--p) // 插入位置及之后的元素右移
   *(p+1)=*p;

变为

 q=&(L.elem[i-1]); // q为插入位置
 for(p=&(L.elem[length-1]);p>=q;--p) // 插入位置及之后的元素右移
     *(p+1)=*p;

5)被放弃的原始基址,并没有如self版本那样做了手动释放?原因是使用了realloc而不是malloc,有关库函数的知识点参见官方文档。
C语言的标准内存分配函数:malloc,calloc,realloc

void* malloc(unsigned size);
在内存的动态存储区中分配一块长度为“size”字节的连续区域(这是一段新的地址),返回该区域的首地址。
void* calloc(size_t numElements, size_t sizeOfElement);
在内存的动态存储区中分配numElements块长度为“sizeOfElement”字节的连续区域,返回首地址,且回内存的指针之前将它初始化为0。
void* realloc(void* ptr, unsigned newsize);

将ptr内存大小增大到newsize。注意realloc将修改一个原先已经分配的内存块的大小,可以使一块内存的扩大或缩小。当起始空间的地址为空,即ptr = NULL,则同malloc。
<1>当
ptr非空:若nuw_size < size,即缩小ptr所指向的内存空间,该内存块尾部的部分内存被拿掉,剩余部分内存的原先内容依然保留;
<2>若nuw_size > size,即扩大
ptr所指向的内存空间,如果原先的内存尾部有足够的扩大空间,则直接在原先的内存块尾部新增内存,如果原先的内存尾部空间不足,或原先的内存块无法改变大小,realloc将重新分配另一块nuw_size大小的内存,并把原先那块内存的内容复制到新的内存块上。因此,使用realloc后就应该改用realloc返回的新指针。

等等,之前self版本实现时用了malloc和memset,还加上一个free,是不是感觉自己有点傻,那就学着改过来,直接用自动挡。

总结下:严版实现还是蛮严谨的,只是某些细节晦涩了点,但处理得更好;self也有自己编码规范的点可取,所以不要排斥这本经典的教材。

(2)Union( ) 求并集

 void Union(SqList &La,SqList Lb) // 算法2.1
 { // 将所有在线性表Lb中但不在La中的数据元素插入到La中
   ElemType e;
   int La_len,Lb_len;
   int i;
   La_len=ListLength(La); // 求线性表的长度
   Lb_len=ListLength(Lb);
   for(i=1;i<=Lb_len;i++){
     GetElem(Lb,i,e); // 取Lb中第i个数据元素赋给e
     if(!LocateElem(La,e,equal)) // La中不存在和e相同的元素,则插入之
       ListInsert(La,++La_len,e);
   }
 }

点开LocateElem( )的实现

int LocateElem(SqList L,ElemType e,Status(*compare)(ElemType,ElemType))
 { // 初始条件:顺序线性表L已存在,compare()是数据元素判定函数(满足为1,否则为0)
   // 操作结果:返回L中第1个与e满足关系compare()的数据元素的位序。
   //           若这样的数据元素不存在,则返回值为0。算法2.6
   ElemType *p;
   int i=1; // i的初值为第1个元素的位序
   p=L.elem; // p的初值为第1个元素的存储位置
   while(i<=L.length&&!compare(*p++,e))
     ++i;
   return (i<=L.length)? i : 0;  //if-else 被我用三目简化了
 }

compare是一个函数指针,用过排序API的人可联想到,一般情况下compare应该返回3个值:0,+1,-1,实际案例中也有不同的实现形式:

 Status compare(ElemType a,ElemType b) {
     return (a==b) ? TRUE : FALSE; //这里 a==b应该推广为equal()
 }

或者

 int compare(ElemType a,ElemType b) {// 根据a<、=或>b,分别返回-1、0或1
   	 int res = a-b;
     return (res==0) ? 0 : res/abs(res);
 }

或者

 int compare(ElemType a,ElemType b){ // 根据a<、=或>b,分别返回-1、0或1
 	 int res = a-b;
 	 if(res == 0)
 	     return 0;
 	  else
 	      return res > 0 ? 1 : -1;
 }

在面对对象语言的编程中,这是一个有意思的话题。至于其他的函数案例,就请各位花适度的时间慢慢磨了,没有必要将自己的数据结构写得很完美,重点还是理解处理问题的思路和一些语法及规范。

1-10 回归数学原理,看书推公式

严版书中P25(算法2.5下方)对插入和删除两种情况作了操作数的期望值讨论,推导如下:
(1)插入问题,假设插入点为i,则
E i t = ∑ i = 1 n + 1 p i ( n − i + 1 ) E_{it}=\sum_{i=1}^{n+1}p_{i}(n-i+1) Eit=i=1n+1pi(ni+1)
随机插入,等概率 p i = 1 n + 1 p_{i}=\frac{1}{n+1} pi=n+11
代入有(求和号内等差数列)
E i t = 1 n + 1 ∑ k = 1 n + 1 ( n − i + 1 ) = n 2 E_{it}=\frac{1}{n+1}\sum_{k=1}^{n+1}(n-i+1)=\frac{n}{2} Eit=n+11k=1n+1(ni+1)=2n
(2)删除问题,假设插入点为i,则
E d l = ∑ i = 1 n q i ( n − i ) E_{dl}=\sum_{i=1}^{n}q_{i}(n-i) Edl=i=1nqi(ni)
随机删除,等概率
q i = 1 n q_{i}=\frac{1}{n} qi=n1
代入有(求和号内等差数列)
E i t = 1 n ∑ i = 1 n ( n − i ) = n − 1 2 E_{it}=\frac{1}{n}\sum_{i=1}^{n}(n-i)=\frac{n-1}{2} Eit=n1i=1n(ni)=2n1
易有时间复杂度 O ( n ) O(n) O(n)

1-11 翻一翻Linux源码

我用3.9.9的内核源码,尝试搜索了array和resize关键词,还真的有类似的实现,在
driver\md目录下,翻看了下资料,收获不小,发现新坑:
(1)先上背景知识
md 是 multiple devices 的缩写,实现了 MD RAID Framework。它将 RAID drivers 跟上层的 block layer 连接在一起,这样可以通过 block layer 访问到底层的 RAID drivers,比如 RAID1/RAID5 driver。同时它作为一个 RAID driver 之上的抽象层,也完成了大量的工作,比如对 superblock 的操作;当然,比较细节的工作还是需要由 RAID driver 自己来完成,毕竟不同 RAID driver 有自己的 RAID 算法需要实现。
(转至:https://www.cnblogs.com/refrag/category/439567.html 烂笔头的博客 《linux-raid》)

磁盘阵列(Redundant Arrays of Independent Disks,RAID)是把相同的数据存储在多个硬盘的不同的地方(因此,冗余地)的方法。通过把数据放在多个硬盘上,输入输出操作能以平衡的方式交叠,改良性能。因为多个硬盘增加了平均故障间隔时间(MTBF),储存冗余数据也增加了容错
(转至:百度百科)

(2)md源码解读,除了上面“烂笔头”的博客,还有一位博主默默努力的小熊“”写了一个系列:《linux内核奇遇记之md(raid)源代码解读》

(3)我通过两位博主的系列贴和自己看源码,虽然没有发现直接使用ArrayList的案例,但某些思路和用法还是类似的(这个才是主线):
在Dm-array.c中,可以看到一个扩容的例子

static int array_resize(struct dm_array_info *info, dm_block_t root,
			uint32_t old_size, uint32_t new_size,
			const void *value, dm_block_t *new_root)
{
	int r;
	struct resize resize;

	if (old_size == new_size)
		return 0;

	resize.info = info;
	resize.root = root;
	resize.size_of_block = dm_bm_block_size(dm_tm_get_bm(info->btree_info.tm));
	resize.max_entries = calc_max_entries(info->value_type.size,
					      resize.size_of_block);

	resize.old_nr_full_blocks = old_size / resize.max_entries;
	resize.old_nr_entries_in_last_block = old_size % resize.max_entries;
	resize.new_nr_full_blocks = new_size / resize.max_entries;
	resize.new_nr_entries_in_last_block = new_size % resize.max_entries;
	resize.value = value;

	r = ((new_size > old_size) ? grow : shrink)(&resize);
	if (r)
		return r;

	*new_root = resize.root;
	return 0;
}

看到grow( ),还是个函数指针,很好有扩容了,但是需要注意这里还有一个shrink( )是缩小block用的,说明无论是简陋的self版本实现还是严版较为严谨的实现,都忽视了当Arraylist实际使用空间远小于给定上限时,需要及时缩小的操作,这个才是看源码的收获!

其实也隐含了一个还能继续挖掘的点:到底怎么扩容和缩小才是合理呢?此处给出两篇对Java中ArrayList的理解:
(1)Arraylist扩容方式的深入个人理解
(2)ArrayList详解及扩容源码分析

先看grow( ):

static int grow(struct resize *resize)
{
	if (resize->new_nr_full_blocks > resize->old_nr_full_blocks)
		return grow_needs_more_blocks(resize);

	else if (resize->old_nr_entries_in_last_block)
		return grow_extend_tail_block(resize, resize->new_nr_entries_in_last_block);

	else
		return grow_add_tail_block(resize);
}

这里为了业务需要,给了三种扩容方式:
(1)直接获取更多:grow_needs_more_blocks( )
(2)扩展尾部: grow_extend_tail_block( )
(3)尾追加/尾插:grow_add_tail_block( )
关于具体的业务细节,光是学习数据结构,我们已经拿到自己希望开眼的部分了,没有必要再深究。

再看shrink( ),几乎都是业务代码。

static int shrink(struct shrinker *shrinker, struct shrink_control *sc)
{
	struct dm_bufio_client *c =
	    container_of(shrinker, struct dm_bufio_client, shrinker);
	unsigned long r;
	unsigned long nr_to_scan = sc->nr_to_scan;

	if (sc->gfp_mask & __GFP_IO)
		dm_bufio_lock(c);
	else if (!dm_bufio_trylock(c))
		return !nr_to_scan ? 0 : -1;

	if (nr_to_scan)
		__scan(c, nr_to_scan, sc);

	r = c->n_buffers[LIST_CLEAN] + c->n_buffers[LIST_DIRTY];
	if (r > INT_MAX)
		r = INT_MAX;

	dm_bufio_unlock(c);

	return r;
}

1-12 总结

  1. 通过对比代码和查看源码,能收获不少语法知识点和DS最重要的思想。
  2. C语言的实现确实比较繁琐,所以建议用Java也写一遍(同C/C++一样,也是编译型语言),可以更深刻的感受每种DS的思想,而少于语法纠结。
  3. 给出一个不错的学习途径(转:刘宇波 老师 的课程,在github上搜 liuyubobobo,代码是开放的,思想需要自己学习);基础稍差的可以尝试慕课或者B站,听一遍也会有新的收获。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值