您的位置:首页 > 职场人生

程序员修炼之路-(2)线性表(上):数组与链表

2015-03-31 21:18 190 查看

1 两块基石

数组与链表构成各种数据结构的基石,是实现所有数据结构必不可少的元素。

1.1 数组

数组一般内置于编程语言中,直接通过索引(index)读写。索引一般为数字,有的语言甚至直接支持如字符串等其他类型的索引。在很多数据结构中都能看到数组的身影,例如字符串、动态数组、堆、栈和队列(用链表也可以,但用数组实现很高效)等。

1.2 链表

概念上都能理解,但实现起来还真有很多容易出错的地方。

实现细节

Ø 表头(header):为什么要添加一个表头?因为有了表头,在第一个结点前添加结点或删除第一结点时会很方便。否则就需要修改客户端持有的链表指针的值,这就要求insert()和delete都要传入二级指针,才能修改客户端持有指针的值。Ø 插入设计:删除结点时一般是指定要删除结点的value,插入则有三种设计:1)追加(append)到链表末尾;2)插入(insert)到指定位置,通过索引或结点的值来指定位置;3)插入(insert)到指定位置,但位置是通过指针来指定。其实,后两种方式差不多,区别就在于是insert()自己调用find()还是客户端先调用一下find()查找,再调用insert()。Ø 删除结点:双向链表就不说了,对于单向链表,删除结点时要遍历至要删除结点的前一个,将这个结点的next域跨过要删除的结点,直接指向要删除结点的next,再释放掉要删除结点占用的内存空间就算完成了。Ø 查找结点:结点值匹配时一定要“==”啊!

C实现

接口定义:结构Node为内部实现,暴露给客户端的是List和Position两个指针。函数包括三块:创建和销毁、增删查、调试打印。由于C语言的限制,元素都只支持int。关于命名,所有函数都使用数据结构名List作为前缀,避免客户端使用时发生命名冲突。


Node结构体:结构体很简单,就是指向下一个结点的指针域next和值域value。


创建和销毁:创建时注意对malloc返回值的判断。销毁时则要注意两点:1)逐个结点销毁,最后销毁头结点;2)使用二级指针:修改客户端持有的List指针的值,避免客户端引用到销毁后的非法内存空间。


增删查:具体注意点在前面已经都提到过,再次强调一下:1)插入的三种设计,这里实现了1追加和3插入到指针指定的位置两种方式;2)对于单向链表,删除时要遍历到前继结点而不是要删除的那个结点;3)查找比较值时一定要“==”。


C++实现

模板类定义:得益于C++的类封装和模板功能,代码变得更加结构化,终于回到了我们熟悉的面向对象世界。因为delete是关键字,所以删除方法改名为remove。


Node类:结点也封装为一个类Node2,这样为每个结点分配内存、初始化成员变量的工作就都统一放到Node2中了,从后面的List2的构造函数中就能明显看到这种优点。


构造和析构:因为Node2类的封装,构造函数变得很简洁。而析构函数则没什么改变,只是free()函数调用变成了delete。


增删查:这次插入和删除该用索引下标的设计方式,所以插入和删除都要遍历到前继结点。


注意wild野指针问题:如果Node2的next忘记初始化为NULL,那么后续的append()等各种遍历操作都会访问到一个非法的内存位置,导致程序意外终止退出。


在Visual Studio中调试方法为:异常退出时VS会提示是否调试,选“是”。控制台输出为非法访问内存位置0x000000043,从链表的各个结点的next值能明显看出,前几个结点的next值为0x006ae开头的内存位置,但最后一个结点的内存位置正是控制台输出的,明显与程序分配内存地址差很远,所以这是野指针导致的,查看代码是否有未赋值NULL的指针即可。

STL实现

(待补充《STL源码剖析》)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: