天天看点

BDS之链表

 链表结构在日常的使用很广泛, 大大小小的软件系统或多或少,或直接或间接的都使用了链表,而关于链表,其又可以分为多种类型,常见的有静态链表,单向链表,双向链表,循环链表等等,除此之外,还有带头结点和不带头结点的链表之分,不过总体来说,这些链表在形态结构上都是类似的。

静态链表

静态链表是用数组实现的链式存储结构,典型的如下:

#define LIST_SIZE   10
typdef unsinged int uInt;
typedef strcut list_node
{
   eType e;
   uInt     next;
}st_link_list[LIST_SIZE];
           

相比于线性顺序存储结构,静态链表的最大优点在于删除和插入元素不需要移动任何元素,只需要修改“指针”,这里并不是真正意思上的指针,而实质上是静态数组的索引。

BDS之链表

 静态链表存储结构图1              

BDS之链表

存储有{a,b}两个元素的静态链表结构图  2     

BDS之链表

  插入元素c后的静态链表结构图3

这里需要说明的是,静态链表将所有的空闲节点组织为备用链表,初始时数组下标为0的单元为该链表的头结点,而st_link_list[LIST_SIZE-1],也就是最后一个元素为链表的头结点,也就是说,静态链表上有两个链表,一个链表上链接的是线性表的结点,另一个链表上链接的是所有未被使用的结点。在图2和图3中我们说明链表加入元素时的过程,开始图2只有a,b两个元素,随后加入元素c后如图3.

单链表

单链表是最基本的具有动态特性的链式存储结构,它的存储结构如下:

typedef struct _list_node
{
   eType data;
   struct _list_node* next;
}list_node,*plist_node;
           

链表中当前的元素的next元素通过指针可以访问下个元素,对于删除和插入的操作,只是对链表的next指针进行操作,同时,由于是动态的增长,单链表比动态链表更加灵活。

BDS之链表

                                                           带有头结点的单向链表示例图

单链表的结构非常相比静态链表简单很多,如上图链表有三个元素4,7,9.

双向链表

双向链表是对单链表进行的拓展,它的存储结构如下:

typedef struct _du_node
{
   eType data;
   struct _du_node* next;
   struct _du_node* prior;
}du_node,*pdu_node;
           

双向链表的节点元素包含了指向前驱节点和后继节点的指针,因此可以通过前驱指针和后继指针访问整个链表,但相比单链表,对于删除和插入操作,相对来说稍微复杂一点。

一般我们讨论的双向链表都是指循环双向链表,它的结构图如下:

BDS之链表

                                                                                双向链表存储结构图

BDS之链表

                                                                   带头结点的循环双链表示例图

循环链表

循环链表是使链表满足环状的特性,它的存储结构和上面介绍的链表结构保持一致,只是将链表中的尾结点(tail)和首结点(head)关联起来,对于双向链表来说,它同样可以通过任何一个结点访问整个链表,并同时具有顺序和逆序访问链表的能力。

一份双向循环链表的简单实现:

#define _NODEBUG
#include "lib_error.h"
#include <assert.h>

#ifndef DU_LINK_H__
#define DU_LINK_H__

typedef int Status;

typedef struct _du_node
{
	eType data;
	struct _du_node* next;
	struct _du_node* prior;
}du_node,*pdu_node;

/*initial link ,create a du_node struct.*/
Status initial(pdu_node* head)
{
	*head = (pdu_node)malloc(sizeof(du_node));
	if(!(*head)){
#ifndef _NODEBUG
		printf("FILE:%s LINE:%d,error msg(code=%d),out of mem in initial method.\n",__FILE__,__LINE__,OUT_OF_MEM);
#endif
		return OUT_OF_MEM;
	}
	(*head)->next=(*head)->prior=*head;
	return STATUS_SUCCESS;
}


/*free the heap node of the dual link*/
Status clear(du_node* head)
{

	du_node* p=head->next;
	du_node* q;	
	while(p!=head){
		q=p;
		p=p->next;
		free(q);
	}
	free(head);
	return STATUS_SUCCESS;
}

/*
* if dual link is empty return true ,else return false
*/
bool is_empty(du_node* head)
{
	return head->next == head ? true:false;
}

/*
*search method: search the special element from the dual link 
*return the pointer of this element or null
*/
pdu_node search(du_node* head,eType e,bool(*eq)(eType e1,eType e2))
{
	du_node* p;
	if(!head) 
		return null;
	p=head->next;		
	while(p!=head){
		if(eq(p->data,e)){
			return p;
		}
		p=p->next;
	}
	return null;
}

/*
*get_elem method: get the element data at the position of index i.
*return : pe stores the return value
*/
Status get_elem(du_node* head,int i,eType* pe)
{
	du_node* p;
	int j=1;
	if(!head)
		return STATUS_ERROR;
	p=head->next; //first node
	while(p!=head&&j<i){
		p=p->next;
		++j;
	}
	if(p==head || j>i)
		return STATUS_NOT_FOUND;
	*pe=p->data;
	return STATUS_OK;
}

/*
*locate_elem: find the first element's index which meet the conditions with comparing function. 
*return :the element's index.
*/
int locate_elem(du_node* head,eType e,Status(*compare)(eType,eType))
{
	du_node* p;
	int i=1;
	if(!head)
		return STATUS_ERROR;
	p=head->next;
	while(p!=head){
		if(compare(p->data,e))
			return i;
		++i;
		p=p->next;
	}
	return STATUS_NOT_FOUND;
}

/*
 *insert method: insert a eType data into link at the index position.
 *return STATUS_SUCCESS if success
*/
Status insert(du_node* head,int i,eType e)
{
	du_node *new_node,*p;
	if(i<0) 
		return INVALID_POSITION;
	new_node=(pdu_node)malloc(sizeof(du_node));
	if(!new_node){
#ifndef _NODEBUG
		printf("FILE:%s LINE:%d,error msg(code=%d),error msg(code=%d):out of mem in initial method.\n",\
				__FILE__,__LINE__,OUT_OF_MEM);
#endif
		return OUT_OF_MEM;
	}
	new_node->data=e;
	if(is_empty(head)){//insert head.
		head->next=new_node;	
		head->prior=new_node;	
		new_node->next=head;
		new_node->prior=head;
		return STATUS_SUCCESS;
	}
	p = head;
	while(i--){
		p=p->next;
	}
	p->prior->next=new_node;
	new_node->prior=p->prior;
	p->prior=new_node;	
	new_node->next=p;	
	return STATUS_SUCCESS;
}

/*
 *delete method: delete a eType data from the dual link
 *return STATUS_SUCCESS if success
*/
Status delnode(du_node* head,eType e)
{
	du_node* p = search(head,e,equal);
	if(!head)
		return STATUS_ERROR;	
	if(p){
		p->prior->next = p->next;
		p->next->prior = p->prior;
		free(p);
		return STATUS_SUCCESS;
	}
	return STATUS_NOT_FOUND;
}

/*
*find_prior: find the prior element 
*return element data.
*/
Status find_prior(du_node* head,eType cur_e,eType* ppe)
{
	du_node* p;
	if(!head)
		return STATUS_ERROR;
	p=head->next->next;//the second node.
	while(p!=head){
		if(p->data==cur_e){
			*ppe=p->prior->data;
			return STATUS_OK;
		}
	}
	return STATUS_NOT_FOUND;
}

/*
*find_next: find the next element
* return element data.
*/
Status find_next(du_node* head,eTeype cur_e,eType* pne)
{
	du_node* p;
	if(!head)
		return STATUS_ERROR;
	p=head->next;//first node.
	while(p->next!=head){
		if(p->data==cur_e){
			*pne=p->next->data;
			return STATUS_OK;
		}
	}
	return STATUS_NOT_FOUND;
}

/*
*traversal: visit each element of the dual link.
*/
void traversal(du_node* head,void(*visit)(eType e))
{
	du_node* p = head->next;
	if(!head)return;
	while(p!=head){
		visit(p->data);
		p=p->next;
	}
}
           

链表的应用

java jdk中最为经典的链表结构要数LinkedList,LinkedList其实是带头结点的双向循环链表的实现。我们看看其主要的源码

public class LinkedList<E> extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    private transient Entry<E> header = new Entry<E>(null, null, null);
    private transient int size = 0;

    /**
     * Constructs an empty list.
     */
    public LinkedList() {
        header.next = header.previous = header;
    }
......
           

从类的声明中不能明显看到循环双链表的结构,而只有一个Entry头元素,Entry是LinkedList的内部类,声明如下:

private static class Entry<E> {
	E element;
	Entry<E> next;
	Entry<E> previous;

	Entry(E element, Entry<E> next, Entry<E> previous) {
	    this.element = element;
	    this.next = next;
	    this.previous = previous;
	}
    }
           

这样看起来更像双向链表的结构,接下来再看下它是如何定位元素,添加元素,删除元素。

private Entry<E> addBefore(E e, Entry<E> entry) {
	Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
	newEntry.previous.next = newEntry;
	newEntry.next.previous = newEntry;
	size++;
	modCount++;
	return newEntry;
    }

    private E remove(Entry<E> e) {
	if (e == header)
	    throw new NoSuchElementException();

        E result = e.element;
	e.previous.next = e.next;
	e.next.previous = e.previous;
        e.next = e.previous = null;
        e.element = null;
	size--;
	modCount++;
        return result;
    }
......
           

这两个私有方法用来进行元素的添加和删除,添加和删除的方法和双链表的删除和增加相同,首先根据元素e创建Entry,在创建过程中就设置了新Entry的前驱和后继元素为entry.previous和entry,随后再将其前驱和后继Entry的后继和前驱分别设置为新的Entry。删除的方法更加明了,就不再赘述。源码中还有一个entry方法用来返回指定索引处指定的Entry元素,这里的索引是相对头元素的。我们看下这个方法:

/**
     * Returns the indexed entry.
     */
    private Entry<E> entry(int index) {
        if (index < 0 || index >= size)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+size);
        Entry<E> e = header;
        if (index < (size >> 1)) {
            for (int i = 0; i <= index; i++)
                e = e.next;
        } else {
            for (int i = size; i > index; i--)
                e = e.previous;
        }
        return e;
    }
           

方法使用了一个聪明的方法来更快的得到Entry,首先判断index索引是否大于size的一半,如果是就按照next来找,否则通过previous来找,这么做的道理很简单,我们总能把搜索范围缩小到n/2,而不是整个链表n.

在STL中同样有关于链表的经典容器List,同linkedlist相似,它同样带有头结点,STL的源码晦涩难懂,这里我只做简单的介绍,说明链表在List容器中的实现原理。

template <class T>  
struct _List_node_base {
  _List_node_base* _M_next;
  _List_node_base* _M_prev;
};
template <class _Tp>
struct _List_node : public _List_node_base {
  _Tp _M_data;
};
           

这是SGI-STL (v3.3) 中List对于结点的定义。对于数据的定义采用了继承方式,区别于java内部类。

iterator insert(iterator __position, const _Tp& __x) {
    _Node* __tmp = _M_create_node(__x);
    __tmp->_M_next = __position._M_node;
    __tmp->_M_prev = __position._M_node->_M_prev;
    __position._M_node->_M_prev->_M_next = __tmp;
    __position._M_node->_M_prev = __tmp;
    return __tmp;
  }
           

这是List类中对于insert的实现部分,这里,我们不要试图去详细了解其中详细的内容和变量类型,不然会陷入无底洞,反而打乱我们的思维,我们的重点应该回到链表上来,对于这段代码,我们能够抽象出其表达的内容就足够了。这里我们用到容器的迭代器,迭代器可以看做是为容器而提供的一种访问数据的标准模型,可以看做是一种广义上的指针类型,可以指向容器的任意元素。一开始同样是创建新节点,然后通过迭代器来更新新节点前后指针,同时更新迭代器指代元素的指针。代码相比LinkedList实现更加清晰和

明了。对于删除元素的操作也是类似,如下:

iterator erase(iterator __position) {
    _List_node_base* __next_node = __position._M_node->_M_next;
    _List_node_base* __prev_node = __position._M_node->_M_prev;
    _Node* __n = (_Node*) __position._M_node;
    __prev_node->_M_next = __next_node;
    __next_node->_M_prev = __prev_node;
    _Destroy(&__n->_M_data);
    _M_put_node(__n);
    return iterator((_Node*) __next_node);
  }
           

或许,真的没有必要介绍这么多重复的东西,说到底都是链表的结构和基本的操作,以至于我不想在这里再介绍linux内核中的链表结构list_head,它的链表操作基本类似,不过很多地方实现的确精妙,有机会再做介绍。当然在List和LinkedList中的很多方法我也没多做介绍,比如 STL List如何实现sort,或者unique,如果你想知道更加详细的内容,那就下载一份源码看看吧!

SGI-STL 下载地址 http://www.sgi.com/tech/stl/download.html

继续阅读