数据结构之——链表(list)、队列(queue)和栈(stack)

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

1)链表

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

1 typedef struct _ListNode{
2     int val;
3     struct _ListNode *next;
4 }ListNode;

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

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

构造一个链表:

1 ListNode *list_init(int arr[],int n){
2     ListNode *h;
3     for(h=NULL;n--;list_append(&h,arr[n]));
4     return h;
5 }

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

...
ListNode *listhead=list_init(arr,n);
ListNode H;
H.next = listhead;
// do some using &H, a list with head nodelist_dosome(&H);
...

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

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指向原来的链表首地址;链表删除,需要注意链表的重复元素,以及当删除的节点为首地址时的情况。

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

1 void list_append(ListNode **head, int val){
2     ListNode *ln=(ListNode*)malloc(sizeof(ListNode));
3     ln->val = val;
4     ln->next = *head;
5     *head = ln;
6 }

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

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

 1 int list_erase(ListNode **head, int val){
 2     int c = 0;
 3     ListNode *t, *h, H;
 4     for(H.next=*head,h=&H,t=h->next;t;t=h->next){
 5         if (t->val==val){
 6             h->next = t->next;
 7             free(t);
 8             ++c;
 9         }
10         else h=t;
11     }
12     *head = H.next;
13     return c;
14 }

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

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

2)队列(queue)

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

 1 typedef struct _QueueInfo{
 2     int *data;
 3     unsigned front, rear;
 4     unsigned capacity;
 5 }QueueInfo;
 6
 7 QueueInfo *queue_init(unsigned int size){
 8     if (size<1) return NULL;
 9     QueueInfo *q = (QueueInfo*)malloc(sizeof(QueueInfo));
10     q->data = (int*)malloc(sizeof(int)*size));
11     q->capacity = size;
12     q->front = q->rear = 0;
13     return q;
14 }

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

Queue Size: 4
Queue 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向后移动即可。

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

 1 int queue_push(QueueInfo *q, int val){
 2     if (q==NULL) return -1;    // need queue
 3     if ((q->rear+1)%q->capacity == q->front) return 0;
 4     q->data[q->rear]=val;
 5     q->rear = (q->rear+1)%q->capacity;
 6     return 1;
 7 }
 8
 9 int queue_pop(QueueInfo *q){
10     if (q==NULL || q->front==q->rear) return 0;
11     q->front = (q->front+1)%q->capacity;
12     return 1;
13 }

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

 1 int queue_front(QueueInfo *q){
 2     if (q==NULL || q->front==q->rear) return 0;
 3     return q->data[q->front];
 4 }
 5
 6 unsigned queue_size(QueueInfo *q){
 7     if (q==NULL) return 0;
 8     if (q->front <= q->rear) return q->rear - q->front;
 9     else return q->capacity - q->front + q->rear - 1;
10 }
11 unsigned queue_capacity(QueueInfo *q){
12     if (q==NULL) return 0;
13     return q->capacity;
14 }

3)栈(stack)

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

 1 typedef struct _StackInfo{
 2     int *data;
 3     unsigned size;
 4     unsigned capacity;
 5 }StackInfo;
 6
 7 StackInfo *stack_init(unsigned capacity){
 8     if (capacity<1) return NULL;
 9     StackInfo *s = (StackInfo*)malloc(sizeof(StackInfo));
10     s->data = (int*)malloc(sizeof(int)*capacity);
11     s->capacity = capacity;
12     s->size = 0;
13     return s;
14 }

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

       init            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

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

 1 int stack_push(StackInfo *s, int val){
 2     if (s==NULL) return -1;    // need stack
 3     if (s->size>=capacity) return 0;
 4     s->data[s->size++]=val;
 5     return 1;
 6 }
 7
 8 int stack_pop(StackInfo *s, int val){
 9     if (s==NULL || s->size<1) return 0;
10     s->size--;
11     return 1;
12 }

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

 1 int stack_top(StackInfo *s){
 2     if (s==NULL || s->size<1) return 0;
 3     return s->data[s->size-1];
 4 }
 5
 6 unsigned stack_size(StackInfo *s){
 7     if (s==NULL) return 0;
 8     return s->size;
 9 }
10
11 unsigned stack_capacity(StackInfo *s){
12     if (s==NULL) return 0;
13     return s->capacity;
14 }

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

时间: 2024-10-12 08:35:08

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

队列 (Queue) 与 栈 (Stack)

队列 (Queue)                                                                                                                                                                                                       队列(Queue)代表了一个先进先出的对象集合.当您需要对各项进行先进先出的访问时

leetcode_103题——Binary Tree Zigzag Level Order Traversal(广度优先搜索,队列queue,栈stack)

Binary Tree Zigzag Level Order Traversal Total Accepted: 31183 Total Submissions: 117840My Submissions Question Solution Given a binary tree, return the zigzag level order traversal of its nodes' values. (ie, from left to right, then right to left fo

用结点实现链表LinkedList,用数组和结点实现栈Stack,用数组和结点链表实现队列Queue

一,用结点实现链表LinkedList,不用换JavaAPI的集合框架 import java.util.Scanner; public class Main { public static class Node { int data; Node next=null; public Node(int data){this.data=data;}; } public static class MyLinkedList { Node head=null; public MyLinkedList()

【Java数据结构学习笔记之二】Java数据结构与算法之队列(Queue)实现

  本篇是数据结构与算法的第三篇,本篇我们将来了解一下知识点: 队列的抽象数据类型 顺序队列的设计与实现 链式队列的设计与实现 队列应用的简单举例 优先队列的设置与实现双链表实现 队列的抽象数据类型   队列同样是一种特殊的线性表,其插入和删除的操作分别在表的两端进行,队列的特点就是先进先出(First In First Out).我们把向队列中插入元素的过程称为入队(Enqueue),删除元素的过程称为出队(Dequeue)并把允许入队的一端称为队尾,允许出的的一端称为队头,没有任何元素的队列

数据结构 - 基于链表的队列

基于链表的队列 当我们基于链表实现队列时,需要从一端加元素,另一端取出元素,就需要引入一个新的变量tail指向链表的尾部,此时,向尾部进行添加操作时间复杂度会变为O(1),然而删除操作还是需要从head向后遍历,所以此时选择链表尾为队尾,链表头为队首. 基于链表的实现的源码如下: package queue; import linkedList.LinkedList; public class LinkedListQueue<E> implements Queue<E> {    

java集合框架:浅谈如何使用LInkedList实现队列(Queue)和堆栈(Stack)

Java中的LinkedList?是采用双向循环列表实现的.利用LinkedList?可以实现栈(stack).队列(queue) 下面写两个例子学生类:int stuId; public int getStuId() { return stuId; } public void setStuId(int stuId) { this.stuId = stuId; } public String getStuName() { return stuName; } public void setStuN

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

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

Java数据结构与算法(4) - 队列(Queue和PriorityQ)

队列: 先进先出(FIFO). 优先级队列: 在优先级队列中,数据项按照关键字的值有序,关键字最小的数据项总在对头,数据项插入的时候会按照顺序插入到合适的位置以确保队列的顺序,从后往前将小于插入项的数据项后移.在图的最小生成树算法中应用优先级队列. 示例代码: package chap04.Queue; class Queue { private int maxSize; private long[] queArray; private int front; private int rear;

java数据结构与算法之(Queue)队列设计与实现

[版权申明]转载请注明出处(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/53375004 出自[zejian的博客] 关联文章: java数据结构与算法之顺序表与链表设计与实现分析 java数据结构与算法之双链表设计与实现 java数据结构与算法之改良顺序表与双链表类似ArrayList和LinkedList(带Iterator迭代器与fast-fail机制) java数据结构与算法之栈设计与实现 java数据结构