链表
有三种链表,单向链表、双向链表、循环链表
循环链表分为单向和双向两种。没有结束,也没有头和尾。主要遍历问题是避免产生循环:如果没有记住从哪里开始,就会在链表中无限循环下去。
链表的基本操作:遍历链表、插入删除链表元素。这些问题总是使用单向链表。
单向链表的重要的一点:维护头指针head和尾指针tail。head丢失会导致链表在内存中丢失。这意味着在进行插入、删除元素操作的时候,如果有必要,必须及时更新链表的head和tail指针。
C语言实现的链表基本操作中,需要注意更新的是否是“本地拷贝”的问题。如下两个方法,只有后者恰当更新了头指针。在C++中,除了本例中采用的传递指向链表头指针的指针方法外,还可以采用传递引用。
2 {
3 IntElement *newElem = new IntElement;
4 if(!newElem) return false;
5 newElem->data = data;
6 newElem->next = head;
7 head = newElem; //Incorrect, head is not update
8 return true;
9 }
10
11 bool insertInFront(IntElement **head, int data);
12 {
13 IntElement *newElem = new IntElement;
14 if(!newElem) return false;
15 newElem->data = data;
16 newElem->next = *head;
17 *head = newElem; //Correctly updates head
18 return true;
19 }
20
栈的链表实现和数组实现
动态数组:动态数组实现的栈必须能根据需要改变大小。优点在于,数组提供对元素的随机访问。但是栈的操作总是在数据结构的一端进行(栈顶),所以数组的随机访问并不能带来好处。另外数组的动态增长导致的数据迁移可能非常耗时。但是如果采用比较良好的方式来减少数组大小的改变次数,较之链表实现的栈速度更快。
链表:链表为每个元素动态分配内存,在处理小规模元素时开销可能很大。链表实现的栈比较简单,面试时候应当选择。
C语言实现基于链表的栈的时候,考虑2个问题:
1. push、pop函数返回值表征操作是否成功,还是push、和pop的data?
2. 本地拷贝的问题,更新head,也就是栈指针,也就是指向链表头元素的指针。
对于第一个问题,C语言实现中,倾向于选择始终让方法返回此操作是否成功。返回的数据则采用这样的方法:传入一个变量的指针。方法可以通过这个指针改变变量的值,从而实现返回数据。
2 typedef struct Element{
3 struct Element *next;
4 void *data;
5 }
6
7 bool push(Element **stack, void *data);
8 bool pop(Element **stack, void **data);
9
另外,栈的实现一般还需要2个函数: bool createStack(Element **stack) 和 bool deleteStack(Element **stack)。createStack只需要简单讲*stack=NULL 既可。后者则需要遍历栈,释放每一个空间。
在面向对象的语言,如C++中,栈的借口设计就更为简单。creatStack和deleteStack变成了构造函数和析构函数。push和pop也和栈对象绑定,不需要将栈作为传入参数,也不需要返回操作是否成功的错误代码,可以让pop直接返回栈中的数据。因为错误都可以通过抛出异常来实现。struct Element则作为类Stack的类型成员。具体代码参考相关资料。
链表中插入、删除元素(C语言)
bool remove ( Element *elem);
bool insertAfter ( Element *elem, int data);
后者能够以NULL作为参数调用,实现在链表头前插入。这些函数成功返回true,失败返回false。编写这两个函数的难点是:保证头指针和尾指针都是当前的。
解决思路是:先写出处理一般链表的代码,之后逐个考虑是否可以满足特殊情况。比如特殊情况有:NULL指针参数,三种可能的链表长度:空、1、2、大于等于3。当链表长度为2时候,remove操作删除第一个元素和删除第二个元素时候head和tail指针的更新。具体参考相关资料。