小肥柴慢慢手写数据结构(C篇)(1-1 线性表 ArrayList 原始版本)
目录
1-1 为啥要有线性表
- 其实就是人们想用数组存一些元素,并提供一些操作,把这两个需求结合在一起做成一个工具,用起来乘手。
- 至于其他XXX特性啊,写完代码再返思效果更好。
1-2 自己出列一个单子(ADT),看看要做什么
- 先画一张图,然后中文写起来:
struct ArrayList {
(1)自定义类型的数组 data[]; //Q1
(2)当前元素个数 len或者size;//Q2
(3)数组最大的容量capacity;//Q3
}
才动笔我们就发现可能存在三个问题:
Q1:这里的数组是否要用动态的?
A1:一开始自己写,代码量少,不用写的那么复杂,先来一款静态的,数组大小用一个宏MAX_SIZE定死,后面再改。
Q2:当前元素个数len是十分重要的一个标记量,相比于当前最后一个元素的数组索引位置last,哪一个更好呢?
A2:其实都差不多,len = last + 1,先用len,很多场合都希望得到当前元素个数。
Q3:最大容量,要不要放在struct中作为标配?
A3:考虑到后续还要修改成动态数组,加上吧,避免一直适用MAX_SIZE重构的时候到处改。
其实真的写完程序了,这些问题都不是核心问题。
- 需要的功能
先列一些基本的功能,做自己有把握很快能实现的,那些高大上的功能后面慢慢磨;细想数组元素的操作,无外乎“增、删、改、查”(CRUD),所以基本功能应该围绕这4个点来讨论。
(1)生成一个ArrayList,最好是带*的,方便函数传参修改内容。
(2)(“增”,add)向当前list中尝试添加一个元素,分为默认在末尾添加和指定位置添加。
(3)(“查”,find/search)在当前list中尝试寻找给定元素item,找到了就返回该元素的位置(实际上是第一次出现的位置)。
(4)(“删”,delete)在当前list尝试删除元素,分为尝试删除给定元素和尝试删除给定位置的元素。
(5)(“改”,update)尝试修改当前list中指定位置的元素。
(6)获取给定位置元素:利用数组的优势,根据索引(index)找到对应的数据。
以上就是咱们的ADT了,注释贴在头文件ArrayList.h中,对照着写代码。
1-3 边想边写
挨个实现,不是大神的就写一段、编一段,测一段。
- ArrayList.h 对照ADT注释编码
typedef int ElementType;
#ifndef _Array_List_h
#define _Array_List_h
#define MAX_SIZE (50)
#define OK (0)
#define ERROR (-1)
/*
struct ArrayList {
(1)自定义类型的数组 data[];
(2)当前元素个数 len或者size;
(3)数组最大的容量capacity;
}
*/
struct ArrayList{
ElementType data[MAX_SIZE];
int len;
int capacity;
};
typedef struct ArrayList *PtrArrayList;
typedef PtrArrayList List;
//(1)生成一个ArrayList,最好是带*的,方便函数传参修改内容。
List createList();
//(2)向当前list中尝试添加一个元素item,分为默认在末尾添加和指定位置pos添加
int addItem(List list, ElementType item, int pos);
int addItemTail(List list, ElementType item);
//(3)在当前list中尝试寻找给定元素item,找到了就返回该元素的位置(实际上是第一次出现的位置)
int findItem(const List list, ElementType item);
//(4)在当前list尝试删除元素,分为尝试删除给定元素item和尝试删除给定位置pos的元素
int removeItem(List list, ElementType item);
int removeByIndex(List list, int pos);
//(5)尝试修改当前list中指定位置pos的元素item
int setItem(List list, ElementType item, int pos);
//(6)尝试获取给定位置pos的元素
ElementType getItem(List list, int pos);
// 可能还有别的功能。。。
#endif
需要注意几个点,代码规范些:
(1)为了防止后头文件被重复引用,用了
#ifndef _Array_List_h
#define _Array_List_h
#endif
的常规处理方式
(2)typedef用于变量名称替换,翻车点(编译问和理解知识点),可以自行百度;通过
typedef struct ArrayList *PtrArrayList;
typedef PtrArrayList List;
操作可隐去部分指针操作,代码易读性友好一些,其实真的猛士会直接上 *PtrArrayList !
(3)定义宏一定要记得用“()”护体,用“ / ”折行
(4)先定两个状态量方便后面用
#define OK (0)
#define ERROR (-1)
- ArrayList.c 对照.h头文件实现每个功能,先来createList()
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ArrayList.h"
List createList(){
List L = (PtrArrayList)malloc(sizeof(struct ArrayList)); //Q4
if(L == NULL){
printf("Out of memery, create List fail\n");
return NULL; //初始化list失败,返回空
}
memset(L->data, 0, MAX_SIZE); //Q5
L->len = 0;
L->capacity = MAX_SIZE;
}
此处两个讨论点:
Q4:malloc前面到底要不要加(xxx *)的强制转换?
A4:网上的答案基本上都能解决问题了,我仅仅想提醒大家这是一个对付编译器的编程习惯而已,实际跑代码过程中,我不加强制转换也是ok哒
Q5:memset到底要不要做
A5:我建议还是要做的,毕竟不希望出现“烫烫烫”,C/C++变量声明一定要初始化,这是好多企业的血泪教训。
还需注意struct中的“.”与“->”两种操作,还是有区别的,我们的实现简单,就没有做太多区别,主要还是看编译器。
此外,网上还有一种讨论:“使用完free(ptr)之后,是否需要将ptr = NULL?”,我个人认为要看具体的场景和语言,给出几篇参考
(1)【C语言】5. 指针free后为什么要刻意指向NULL、野指针(原因、解决)、悬垂指针
(2)为什么 C 语言的 free 函数最后不自己把指针指向 NULL? ⇒ 经典话语:“在有经验的程序员看来,你这个蠢操作把错误隐藏的更深、导致程序逻辑更复杂、更难排除错误了”
(3)内存管理:C语言中的Malloc/free是如何分配内存的
别着急往下编,先来个main()试一把createList(),及时排错,main.c:
int main(int argc, char *argv[]) {
int i;
List list = createList();
printf("\nlist is empty? %d\n", isEmpty(list));
return 0;
}
isEmpty()用于检测当前list是否完成初始化(指针不为NULL,data[]没有数据,len=0),真的猛士会直接debug检查。
ArrayList.h中添加定义
int isEmpty(List list);
ArrayList.c中添加实现
int isEmpty(List list){
if(list == NULL)
return ERROR;
return (list->len==0);
}
- 完成add工作,在尾部插入相对简单,我们思考下指定位置添加元素,先画个图
相当于所有元素向后挪一位,注意最后一个元素的角标处理和元素个数增加(len++);还要考虑(1)list为空(2)数组满了len==capacity(下个贴我们需要改进的点)(3)插入位置越界(pos<0 or pos>=capacity)三种无效/非法操作。
int addItem(List list, ElementType item, int pos){
int i;
if(list == NULL){
printf("\nlist is null\n");
return ERROR;
} else if(list->len == list->capacity){
printf("\nlist is full!\n");
return ERROR;
} else if(pos < 0 || pos >= list->capacity){
printf("\npos out of range!\n");
return ERROR;
}
for(i = list->len-1; i >= pos; i--)
list->data[i+1] = list->data[i];
list->data[pos] = item;
list->len++;
return OK;
}
此处有两个讨论点:
P1:非法判断那么多条件,要不要写成一个函数呢?我认为是可行的,最好是看已经成型的开源库中他人的写法,这种讨论要适度。
P2:挪位置的循环操作中,到底是从最后一位last索引开始迭代,还是从pos索引开始迭代呢?其实选择从后面开始比较妥当,处理起来方便很多,不相信可以尝试从pos迭代。
- 默认添加元素方法addItemTail(),直接使用现有addItem()
int addItemTail(List list, ElementType item){
return addItem(list, item, list->len);
}
- 寻找元素findItem()
int findItem(const List list, ElementType item){
int i = 0;
if(list == NULL)
return ERROR;
// for(i = 0; i < list->len; i++){
// if(list->data[i] == item)
// return i;
// }
// return ERROR;
while(i < list->len && list->data[i] != item)
i++;
return i > (list->len - 1) ? ERROR : i;
}
(1)注意const的用法,就是不希望list内容被修改,在很多源码中都是常见操作
(2)此处讨论下到底用while好,还是用for好。其实都差不多,while看起来清爽些,大佬们也爱用,for对初学者有好些,条件比较次数其实都是一样的。
- 删除元素的实现 removeItem()与removeByIndex(),先画图
int removeItem(List list, ElementType item){
int i;
if(list == NULL){
printf("\nlist is null\n");
return ERROR;
}
int pos = findItem(list, item);
if(pos != ERROR){
for(i = pos; i < list->len-1; i++)
list->data[i] = list->data[i+1];
list->data[list->len-1] = 0;
list->len--;
return pos;
}
return ERROR;
}
int removeByIndex(List list, int pos){
int i;
if(list == NULL){
printf("\nlist is null\n");
return ERROR;
} else if(pos < 0 || pos >= list->len){
printf("\npos out of range!\n");
return ERROR;
}
for(i = pos; i < list->len-1; i++)
list->data[i] = list->data[i+1];
list->data[list->len-1] = 0;
list->len--;
return OK;
}
如果不考虑输入异常,这两个功能是可以部分代码复用的;特别对于尝试删除元素的操作,其实是删除了list中第一个出现的该元素,我们的list是允许有重复元素存在的,这个操作的第一步就是通过find函数找到目标元素的索引位置。
- 接下来就是改和获取了
int setItem(List list, ElementType item, int pos){
if(list == NULL){
printf("\nlist is null\n");
return ERROR;
} else if(pos < 0 || pos >= list->len){
printf("\npos out of range!\n");
return ERROR;
}
list->data[pos] = item;
return OK;
}
ElementType getItem(List list, int pos){
if(list == NULL){
printf("\nlist is null\n");
return ERROR;
} else if(pos < 0 || pos >= list->len){
printf("\npos out of range!\n");
return ERROR;
}
return list->data[pos];
}
- 别忘了打印功能
void printList(const List list){
int i;
if(list != NULL){
printf("\n[ ");
for(i = 0; i < list->len; i++)
printf("%d ", list->data[i]);
printf("]\n");
}
}
- 最后统一贴一下各个文件的实现:
ArrayList.h
typedef int ElementType;
#ifndef _Array_List_h
#define _Array_List_h
#define MAX_SIZE (50)
#define OK (0)
#define ERROR (-1)
struct ArrayList{
ElementType data[MAX_SIZE];
int len;
int capacity;
};
typedef struct ArrayList *PtrArrayList;
typedef PtrArrayList List;
List createList();
int addItem(List list, ElementType item, int pos);
int addItemTail(List list, ElementType item);
int findItem(const List list, ElementType item);
int removeItem(List list, ElementType item);
int removeByIndex(List list, int pos);
int setItem(List list, ElementType item, int pos);
ElementType getItem(List list, int pos);
int isEmpty(List list);
void printList(const List list);
#endif
ArrayList.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ArrayList.h"
List createList(){
List L = (PtrArrayList)malloc(sizeof(struct ArrayList));
if(L == NULL){
printf("Out of memery, create List fail\n");
return NULL;
}
memset(L->data, 0, MAX_SIZE);
L->len = 0;
L->capacity = MAX_SIZE;
return L;
}
int addItem(List list, ElementType item, int pos){
int i;
if(list == NULL){
printf("\nlist is null\n");
return ERROR;
} else if(list->len == list->capacity){
printf("\nlist is full!\n");
return ERROR;
} else if(pos < 0 || pos >= list->capacity){
printf("\npos out of range!\n");
return ERROR;
}
for(i = list->len-1; i >= pos; i--){
list->data[i+1] = list->data[i];
}
list->data[pos] = item;
list->len++;
return OK;
}
int addItemTail(List list, ElementType item){
return addItem(list, item, list->len);
}
int findItem(const List list, ElementType item){
int i = 0;
if(list == NULL)
return ERROR;
// for(i = 0; i < list->len; i++){
// if(list->data[i] == item)
// return i;
// }
// return ERROR;
while(i < list->len && list->data[i] != item)
i++;
return i > (list->len - 1) ? ERROR : i;
}
int removeItem(List list, ElementType item){
int i;
if(list == NULL){
printf("\nlist is null\n");
return ERROR;
}
int pos = findItem(list, item);
if(pos != ERROR){
for(i = pos; i < list->len-1; i++)
list->data[i] = list->data[i+1];
list->data[list->len-1] = 0;
list->len--;
return pos;
}
return ERROR;
}
int removeByIndex(List list, int pos){
int i;
if(list == NULL){
printf("\nlist is null\n");
return ERROR;
} else if(pos < 0 || pos >= list->len){
printf("\npos out of range!\n");
return ERROR;
}
for(i = pos; i < list->len-1; i++)
list->data[i] = list->data[i+1];
list->data[list->len-1] = 0;
list->len--;
return OK;
}
int setItem(List list, ElementType item, int pos){
if(list == NULL){
printf("\nlist is null\n");
return ERROR;
} else if(pos < 0 || pos >= list->len){
printf("\npos out of range!\n");
return ERROR;
}
list->data[pos] = item;
return OK;
}
ElementType getItem(List list, int pos){
if(list == NULL){
printf("\nlist is null\n");
return ERROR;
} else if(pos < 0 || pos >= list->len){
printf("\npos out of range!\n");
return ERROR;
}
return list->data[pos];
}
int isEmpty(List list){
if(list == NULL)
return ERROR;
return (list->len==0);
}
void printList(const List list){
int i;
if(list != NULL){
printf("\n[ ");
for(i = 0; i < list->len; i++)
printf("%d ", list->data[i]);
printf("]\n");
}
}
调用和测试main.c,可以随便写,但注意需要测试边界场景
#include <stdio.h>
#include <stdlib.h>
#include "ArrayList.h"
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int main(int argc, char *argv[]) {
int i;
List list = createList();
printf("\nlist is empty? %d\n", isEmpty(list));
for(i = 0; i < 20; i++)
addItemTail(list, i);
printf("\ninit data: len=%d\n", list->len);
printList(list);
printf("\n==============test add item===================\n");
addItemTail(list, 100);
printList(list);
addItem(list, 200, 3);
printList(list);
addItem(list, 77, 0);
printList(list);
addItem(list, 99, MAX_SIZE);
printList(list);
addItem(list, 99, -1);
printList(list);
printf("\n==============test find item===================\n");
printf("\nfind 77'pos=%d\n", findItem(list, 77));
printf("\nfind 100'pos=%d\n", findItem(list, 100));
printf("\nfind 20'pos=%d\n", findItem(list, 20));
printf("\nfind 99'pos=%d\n", findItem(list, 99));
printf("\n==============test remove item===================\n");
printf("\nremove 10 =>pos=%d\n", removeItem(list, 10));
printList(list);
printf("\nremove 10 again =>pos=%d\n", removeItem(list, 10));
printList(list);
printf("\nremove 77 =>pos=%d\n", removeItem(list, 77));
printList(list);
printf("\nremove 100 =>pos=%d\n", removeItem(list, 100));
printList(list);
printf("\nremove pos=0, ret=%d\n", removeByIndex(list, 0));
printList(list);
printf("\nremove pos=10, ret=%d\n", removeByIndex(list, 10));
printList(list);
printf("\nremove pos=%d, ret=%d\n", list->len-1, removeByIndex(list, list->len-1));
printList(list);
printf("\nremove pos=%d, ret=%d\n", list->len, removeByIndex(list, list->len));
printList(list);
printf("\nremove pos=%d, ret=%d\n", MAX_SIZE, removeByIndex(list, MAX_SIZE));
printList(list);
printf("\n==============test set item===================\n");
printf("\nset -2 pos=%d ~~~ %d \n", 0, setItem(list, 0, -2));
printList(list);
printf("\nset -10 pos=%d ~~~ %d \n", list->len-1, setItem(list, -10, list->len-1));
printList(list);
printf("\nset -100 pos=%d ~~~ %d \n", 7, setItem(list, -100, 7));
printList(list);
printf("\nset -50 pos=%d ~~~ %d \n", -1, setItem(list, -50, -1));
printList(list);
printf("\nset -60 pos=%d ~~~ %d \n", MAX_SIZE, setItem(list, -60, MAX_SIZE));
printList(list);
printf("\n==============test get item===================\n");
printf("get pos=>%d = %d\n", 0, getItem(list, 0));
printf("get pos=>%d = %d\n", -1, getItem(list, -1));
printf("get pos=>%d = %d\n", list->len-1, getItem(list, list->len-1));
printf("get pos=>%d = %d\n", MAX_SIZE, getItem(list, MAX_SIZE));
printf("\nlist is empty? %d\n", isEmpty(list));
return 0;
}
(闲得慌)可以尝试实现不同语言版本,顺便体会下它们的魅力,多看源码
1-4 反思一下
- 总感觉各种非法判断,把程序可读性降低了,有待改进。
- 写死大小的数组需变成动态的会更好用。
- 至于那些加入一个集合/数组/队列的初始化方式,有着丰富的变形,暂不考虑;类似于合并两个list的方法,sort排序等等问题,大家可以继续修改尝试,但最好还是等动态数组完成后再改更好。
- 明显可以看到,因为有数组的索引,ArrayList在获取元素方面(也就是查的能力)速度是有保障的(依靠索引),但缺点也很明显:插入和删除成本高;具体的定量讨论,等完成了单链表后再做。
- 不明白的语法细节,多看多记就行,但不要放过这些细节,面试会多少问道。