在前面几篇博文中曾经提到链表(list)、队列(queue)和(stack),为了更加系统化,这里统一介绍着三种数据结构及相应实现。

1)链表

首先回想一下基本的数据类型,当需要存储多个相同类型的数据时,优先使用数组。数组可以通过下标直接访问(即随机访问),正是由于这个优点,数组无法动态添加或删除其中的元素,而链表弥补了这种缺陷。首先看一下C风格的单链表节点声明:

// single list node define
typedef struct __ListNode
{
int val;
struct __ListNode *next;
}ListNode;

所谓单链表,即只有一个指针,该指针指向下一个元素的地址。通常只要知道链表首地址,则可以遍历整个链表。由于链表节点是在堆区动态申请的,其地址并不是连续的,因此无法进行随机访问,只有通过前一节点的next指针才能定位下一节点的地址。

单链表只能向后遍历,无法逆序遍历,因此诞生了使用更广泛的双链表,即节点内部增加一个字段*prev,用以存储该节点的前一个节点地址。双链表可以双向遍历,但仍然只能顺序访问,无法像数组那样随机访问。以下均以单链表为例介绍其构造、插入和删除。

构造一个链表:

// Init single list without head node
ListNode *list_init(int arr[], int n)
{
ListNode *h;
for(h=NULL; n--; list_append(&h,arr[n]));
return h;
}

注意,带头节点的链表可以简化大量操作,因此有些链表操作需要头结点(头结点不存储链表内容)。此处返回一个不带头结点的链表,当需要头结点时,可以新建一个节点,并将其next指向该链表首地址,但这并不意味着链表头节点存储在堆区。换言之,链表头节点可以存储在栈区(注意这里的堆区和栈区指内存中的特定区域,非数据结构中的堆和栈)。一种典型的使用是:

// Using Virtual Head which stored in stack memory not in heap memory
...
ListNode *listhead=list_init(arr,n);
ListNode H; // stored in stack memory not in heap memory
H.next = listhead;
list_dosome(&H);// do some operator by using &H which contains head node
...

当上面执行完毕,会自动将栈区的数据进行释放,则H节点会自动释放,其next地址作为真正的链表首地址。这样的好处是,代码结构与有头结点的链表操作一样简单,并且头结点不会永久占用内存空间,达到随时使用,随时“申请”的效果。如不加特殊说明,下面均以不带头结点的链表进行操作。

/* Single list append and erase node sketch
* ListNode *h=0x0010;
* List: 15->20->10->15->NULL
* addr: 0x0010 +-> 0x2010 +-> 0x1014 +-> 0x0200
* val: 15 | 20 | 10 | 15
* next: 0x2010 -+ 0x1014 -+ 0x0200 -+ 0x0000 -->NULL
*
* List: 15->20->10->15->NULL
* append:7
* addr: 0x3014 +-> 0x0010 +-> 0x2010 +-> 0x1014 +-> 0x0200
* val: 7 | 15 | 20 | 10 | 15
* next: 0x0010 -+ 0x2010 -+ 0x1014 -+ 0x020- -+ 0x0000 -->NULL
* ListNode *h=0x3014; h->next = 0x0010;
* List: 7->15->20->10->15->NULL
*
* List: 15->20->10->15->NULL
* erase: 15
* addr: 0x2010 +-> 0x1014
* val: 20 | 10
* next: 0x1014 -+ 0x0000 -->NULL
* ListNode *h=0x2010;
* List: 20->10->NULL
* */

如图,插入节点使用头插入法,因此插入7时,需要将链表首地址更改为7的地址,并将其next指向原来的链表首地址;链表删除,需要注意链表的重复元素,以及当删除的节点为首地址时的情况。

下面给出一种单链表的插入节点方法:

// list append, using head insertion
void list_append(ListNode **head, int val)
{
ListNode *ln = (ListNode*)malloc(sizeof(ListNode));
ln->val = val;
ln->next = *head;
*head = ln;
}

单链表的插入方法有头插入和尾插入,前者将新节点插入到链表开始位置,后者将新节点插入到尾部。通常只给出链表首地址,所以上面提供了头插入方法。注意,此处链表插入的参数为二级指针,为什么这样操作?因为,每次插入时,链表首地址将发生改变(假如一个链表带头结点,则不需这种处理)。也可通过返回值回传新的链表首地址,然而每次插入一个节点,都要将新链表地址重新写回(请回想二叉树的插入方法)。

单链表的删除操作略有不同,链表节点可能存在重复,因此需要删除所有为给定值的节点,如下代码,其返回值为删除的节点个数(0表示没有找到该节点):

// list erase, may erase first node
int list_erase(ListNode **head, int val)
{
int c = 0;
ListNode *t, *h, H;
for(H.next=*head, h=&H, t=h->next; t; t=h->next){
if (t->val == val){
h->next = t->next;
free(t);
++c; // may have several node value equal to val
}
else h=t;
}
*head = H.next;
return c; // return erase count
}

如果一个链表有两个节点,其值均为10,而此时需要删除10,那么就要处理链表首地址为待删除节点的情况。上面代码同样需要传入二级链表首地址。注意5~14行,这里就是在栈区添加一个头节点,方便了大量操作。

最后,当一个链表确定不再需要时,请不要忘记将其释放掉,并将链表首地址指向NULL。

2)队列(queue)

队列即按照数据到达的顺序进行排队,每次新插入一个节点,将其插到队尾;每次只有对头才能出队列。简言之,对于数据元素的到达顺序,做到“先进先出”。由于队列通常频繁的插入与删除,为了高效,一般使用固定长度的数组进行实现,并且可循环使用数组空间,所以要经常处理当前队列是否为满或为空。如需要动态长度,可以用链表实现,只需要同时记住链表首地址(队列的头)和尾地址(队列的尾)。下面使用定长数组实现一个循环队列:

// recyle queue struct to storage information
typedef struct __QueueInfo{
int *date;
unsigned int front, rear;
unsigned int capacity;
}QueueInfo; // recyle queue initializatio
QueueInfo *queue_init(unsigned int size)
{
if (size < 1) return NULL;
QueueInfo *q = (QueueInfo*)malloc(sizeof(QueueInfo));
q->data = (int*)malloc(sizeof(int)*size);
q->capacity = size;
q->front = q->rear = 0;
return q;
}

其中使用QueueInfo存储当前队列的一些信息,data为动态申请的连续的队列空间,front指向队列头,rear为队列尾部,capacity为队列可容纳的大小。初始化时,将front与rear都置为0。由于是循环使用队列空间,当逐渐入队capacity个元素时,此时front超过了队列容量,需要将其重置到0位置,这样将无法判断当前队列是满还是空。一种解决办法是,仅使用capacity-1个空间进行存储,始终保持front与rear之间存在不小于1个可用空间,此方法与链表的头节点有异曲同工之妙。

/* Recyle Queue Operator Push and Pop Sketch
* Queue Size: 4
* Capacity : 7
* front rear front rear rear front
* | | | | | |
* 1 2 3 4[ ][ ][ ] [ ]1 2 3 4[ ][ ] 4[ ][ ][ ]1 2 3
* (1) (2) (3)
*
* PUSH : 5
* front rear front rear rear front
* | | | | | |
* 1 2 3 4 5[ ][ ] [ ]1 2 3 4 5[ ] 4 5[ ][ ]1 2 3
* (1+) (2+) (3+)
*
* POP :
* front rear front rear rear front
* | | | | | |
* [ ]2 3 4[ ][ ][ ] [ ][ ]2 3 4[ ][ ] 4[ ][ ][ ][ ]2 3
* (1-) (2-) (3-)
* */

对于同样容量为7,大小为4的循环队列,有以上三种情况。所以当判断队列是否为空、或者是否有可用空间时,切勿直接判断front与rear的大小。因此,当进行入队和出队时,也要针对不同情况进行处理。每次入队时,将元素覆盖在rear处,并将rear后移一位,注意判断队列为空还是满,并且保证其不大于capacity。出队则从队头删除,只需将front向后移动即可。

下面是队列的插入、删除:

// recyle queue push
int queue_push(QueueInfo *q, int val)
{
if (q==NULL) return -1; // need queue
if ((q->rear+1)%q->capacity == q->front) return 0;
q->data[q->rear] = val;
q->rear = (q->rear+1)%q->capacity;
return 1; // return push count
} // recyle queue pop
int queue_pop(QueueInfo *q)
{
if (q==NULL || q->front==q->rear) return 0;
q->front = (q->front+1)%q->capacity;
return 1; // return pop count
}

通常为了便于调用使用,一般提供访问当前队列的队头,和获取队列大小、容量信息,如下:

// get queue front
int queue_front(QueueInfo *q)
{
if (q==NULL || q->front==q->rear) return 0;
return q->data[q->front];
}
// get queue size
unsigned int queue_size(QueueInfo *q)
{
if (q==NULL) return 0;
if (q->front <= q->rear) return q->rear - q->front;
else return q->capacity - q->front + q->rear - 1;
}
// get queue capacity
unsigned int queue_capacity(QueueInfo *q)
{
if (q==NULL) return 0;
return q->capacity;
}

3)栈(stack)

栈的特点与队列正好相反,按照数据入栈顺序逆序出栈,即“后进先出”。每次入栈将元素放在栈顶,出栈时从栈顶开始出栈。通常会对栈进行频繁入栈和出栈,与队列类似,一般使用定长数组存储栈元素,而不是动态申请节点空间。同样给出栈的定义和初始化代码:

// easy stack struct to storage stack information
typedef struct __StackInfo
{
int *data;
unsigned int size;
unsigned int capacity;
}StackInfo; // stack init, size=0
StackInfo *stack_init(unsigned int capacity)
{
if (capacity < 1) return NULL;
StackInfo *s = (StackInfo*)malloc(sizeof(StackInfo));
s->data = (int*)malloc(sizeof(int)*capacity);
s->capacity = capacity;
s->size = 0;
return s;
}

与队列类似,使用一个结构体存储当前栈的大小和容量。由于入栈和出栈都在栈顶,所以只需要一个size字段存储当前栈的大小。每次入栈时,将size向后移动;出栈时将size向前移动,注意不要超过容量,初始化size为0。

/* easy stack push and pop sketch
* initialize push:4 pop
* Capacity: 7 7 7
* Size : 3 4 2
* Top<---- [ ] [ ] [ ]
* [ ] [ ] [ ]
* [ ] size<--- [ ] [ ]
* size<--- [ ] 4 [ ]
* 3 3 size<--- [ ]
* 2 2 2
* Bottom<-- 1 1 1
* */

下面给出入栈出栈的一种实现:

// stack push
int stack_push(StackInfo *s, int val)
{
if (s==NULL) return -1; // need stack
if (s->size >= s->capacity) return 0;
s->data[s->size++] = val;
return 1; // return push count
} // stack pop
int stack_pop(StackInfo *s)
{
if (s==NULL || s->size<1) return 0;
s->size--;
return 1; // return pop count
}

栈的操作比较简单,只有一个指针size,并且不需要循环操作。通常也需要获取当前栈的大小等信息,如下:

// get stack top
int stack_top(StackInfo *s)
{
if (s==NULL || s->size<1) return 0;
return s->data[s->size-1];
} // get stack size
unsigned int stack_size(StackInfo *s)
{
if (s==NULL) return 0;
return s->size;
} // get stack capacity
unsigned int stack_capacity(StackInfo *s)
{
if (s==NULL) return 0;
return s->capaccity;
}

链表、队列和栈的概念介绍完毕,虽然很简单,但是就像数组那样简单而又广泛使用。以上均为C风格代码,对于C++风格并没介绍。因为STL中已经包含了这三种数据结构,并使用模板类进行书写。其中队列和栈为动态增长的,不必要初始其容量。当需要使用这三种数据结构时,优先使用STL提供的代码,而不是自己动手实现。

注:本文涉及的源码:single list : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/list/singlelist.c

recyle  queue : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/queue/recylequeue.c

esay stack : https://git.oschina.net/eudiwffe/codingstudy/blob/master/src/stack/easystack.c

[数据结构]——链表(list)、队列(queue)和栈(stack)的更多相关文章

  1. c++中队列queue和栈stack的基本操作

    1.queue 模板类的定义在<queue>头文件中. 定义queue 对象的示例代码如下:queue<int> q1;queue<double> q2; queu ...

  2. C# 队列(Queue) 和堆栈(Stack)

    队列 (Queue)                                                                                          ...

  3. 队列Queue和栈

    1.队列Queue是常用的数据结构,可以将队列看成特殊的线性表,队列限制了对线性表的访问方式,只能从线性表的一段添加(offer)元素, 从另一段取出(poll)元素,队列遵循先进先出的原则. 2.J ...

  4. [数据结构与算法]队列Queue 的多种实现

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  5. 队列Queue、栈LifoQueue、优先级队列PriorityQueue

    队列:队列是先进先出. import queue q = queue.Queue() q.put(1) q.put(2) q.put(3) q.put(4) print(q.get()) print( ...

  6. Airport Simulation (数据结构与算法 – 队列 / Queue 的应用)

    Airport Simulation 是数据结构与算法教材中用于演示Queue的一个小程序(大多数教师似乎会跳过这个练习).主程序会通过输入总的运行时间.队列里可以等待的最多飞机数量,平均每个时间单元 ...

  7. 数据结构与算法--堆(heap)与栈(stack)的区别

    堆和栈的区别 在C.C++编程中,经常需要操作的内存可分为以下几个类别: 栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构中的栈. 堆区(heap ...

  8. 堆heap和栈Stack(百科)

    堆heap和栈Stack 在计算机领域,堆栈是一个不容忽视的概念,堆栈是两种数据结构.堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除.在单片机应用中,堆栈 ...

  9. python基本数据结构栈stack和队列queue

    1,栈,后进先出,多用于反转 Python里面实现栈,就是把list包装成一个类,再添加一些方法作为栈的基本操作. 栈的实现: class Stack(object): #初始化栈为空列表 def _ ...

随机推荐

  1. java: web应用中不经意的内存泄露

    前面有一篇讲解如何在spring mvc web应用中一启动就执行某些逻辑,今天无意发现如果使用不当,很容易引起内存泄露,测试代码如下: 1.定义一个类App package com.cnblogs. ...

  2. Cassandra简介

    在前面的一篇文章<图形数据库Neo4J简介>中,我们介绍了一种非常流行的图形数据库Neo4J的使用方法.而在本文中,我们将对另外一种类型的NoSQL数据库——Cassandra进行简单地介 ...

  3. SSH实战 · 唯唯乐购项目(上)

    前台需求分析 一:用户模块 注册 前台JS校验 使用AJAX完成对用户名(邮箱)的异步校验 后台Struts2校验 验证码 发送激活邮件 将用户信息存入到数据库 激活 点击激活邮件中的链接完成激活 根 ...

  4. JAVA for mac 的学习之路

    要学习一门新技术,首先得下载相关的工具. 一 . 下载相关工具 1. 下载 jdk formac 下载地址为:http://www.oracle.com/technetwork/java/javase ...

  5. 07.LoT.UI 前后台通用框架分解系列之——强大的文本编辑器

    LOT.UI分解系列汇总:http://www.cnblogs.com/dunitian/p/4822808.html#lotui LoT.UI开源地址如下:https://github.com/du ...

  6. 如何利用tcpdump对mysql进行抓包操作

    命令如下: tcpdump -s -l -w - dst -i eno16777736 |strings 其中-i指定监听的网络接口,在RHEL 7下,网络接口名不再是之前的eth0,而是 eno16 ...

  7. 【定有惊喜】android程序员如何做自己的API接口?php与android的良好交互(附环境搭建),让前端数据动起来~

    一.写在前面 web开发有前端和后端之分,其实android还是有前端和后端之分.android开发就相当于手机app的前端,一般都是php+android或者jsp+android开发.androi ...

  8. 利用Select2优化@Html.ListBoxFor显示,学会用MultiSelectList

    最近需要用到多选框,Asp.Net MVC自带的@Html.ListBox或@Html.ListBoxFor的显示效果太差,于是找到了Select2进行优化,并正式了解了多选框的操作方法. 首先介绍多 ...

  9. 转:ORA-15186: ASMLIB error function = [asm_open], error = [1], 2009-05-24 13:57:38

    转:ORA-15186: ASMLIB error function = [asm_open], error = [1], 2009-05-24 13:57:38http://space.itpub. ...

  10. FineReport如何用JDBC连接阿里云ADS数据库

    在使用FineReport连接阿里云的ADS(AnalyticDB)数据库,很多时候在测试连接时就失败了.此时,该如何连接ADS数据库呢? 我们只需要手动将连接ads数据库需要使用到的jar放置到%F ...