今天主要给大家介绍几种数据结构,这几种数据结构在实现原理上较为类似,我习惯称之为类list的容器。具体有list、stack以及queue。

list的节点Node

首先介绍下node,也就是组成list的节点。从面向对象的角度来说节点也是就一个类,list里面包含了node对象的实例,以及操作/管理这些实例的方法。先给出一个粗糙的node的C++代码如下代码块所示。可以看出除了保有当前节点的信息,其还有指向前一个节点和后一个节点的指针。如果单独使用当然另外还需要相应的构造函数。如果是使用node组成其他的如list数据结构,也可以不需要node自己的构造函数,而完全交由list分配空间和数据,这部分可以参考《STL源码剖析》。好吧,扯得有点远了,为了讲述的简单,后面的代码都将采取简易的实现方式,如有不妥欢迎私信。

template<typename T>
struct Node
{
typedef void * void_pointer;
void_pointer prev;
void_pointer next;
T data; Node(T data){
this->data = data;
prev = nullptr;
next = nullptr;
}
};

Node构成的数据结构有哪些

说完了node,那么由node(或其变体)作为基本单元的数据结构有哪些呢?当然首先有list(链表),其次stack(栈)以及queue(队列)也是哈。list是双向链接的,头和尾都可以插入和删除node,当然中间插入删除也是可以的。而stack则是先进后出的队列,啥意思呢,也就是说:插入和删除都是针对最顶层的node的;queue则不同,它是先进先出的,和我们日常排队买东西是一样的原理。接下来分别介绍。

list

先来个图感性认识一下:

其实看了图之后也就无需多言了哈,不就是node一个链接一个么。嘿,简单!!!

stack

那stack呢,更加简单啦。只要将双向的链表换成单向的链表就可以实现了哈。在表的前端插入来实施push操作,通过删除表的前端元素来完成pop操作。top操作知识考察表前端元素并返回它的值。(当然stack也可以用基于vector来实现)。

queue

先进先出,其实现可以基于链表。但是也可以基于deque/vector。与stack一样若基于已有的结构,其实现是较为简单的,这里不详细叙述。

实现一个list

其实从stl的源码可以看出,要真正实现一个list并不像看上去那么简单。考虑到内存分配以及构造的细节,还是比较麻烦的。这里我们主要是为了掌握list的基本结构,对其主要操作做一个了解。所以不必写那么复杂的实现(哈哈,太复杂的我也写不好,还是建议阅读源码哈)。

list的迭代器

迭代器是后需几乎所有操作的基础。那么怎么组织这个迭代器呢,或者换句换说,迭代器在哪里定义呢?我们可以这么组织我们的代码:

using namespace std;

template <typename Object>
class List
{
private:
struct Node
{
...
}; public:
class const_iterator
{
public:
...
protected:
Node *current;
...
friend class List<Object>;
}; class iterator : public const_iterator
{
public:
...
friend class List<Object>;
}; public:
List( )
{ init( ); }
~List( )
//其他方法:
...
...
private:
int theSize;
Node *head;
Node *tail; void init( )
{
theSize = 0;
head = new Node;
tail = new Node;
head->next = tail;
tail->prev = head;
}
};

稍微分析一下,首先list由node一个链接一个构成,所以需要一个node类。另外不希望第三方类使用,所以是私有的。在看迭代器,迭代器是为了方便遍历list或者指代list的当前位置的。所以其本质是一个指针,而且是指向node的指针,所以可以看到const_iterator类有个指针的定义:Node *current;。因为有时候通过迭代器只是想单纯访问数据而已,而有时候是希望能够改变node节点的数据。所以就有了const_iterator以及iterator两套哈。我不打算在这里给出迭代器的实现细节,这部分内容有很多教材的源码都是不错的,STL源码可能复杂些不建议。推荐《数据结构与算法分析--C++语言描述(第四版)》,请参考我的GitHub。但是还是有几个细节需要提一下:

  • 迭代器的类要是list的友元。
  • 迭代器在某种意义上就是某种智能指针,所以重载*以及->(或者++, --)操作是必须的。
  • 为了后续操作的方便,在list中==以及!=通常也少不了。
  • 通常需要head(表示第一个元素的前一个位置)以及tail(最后一个元素的最后一个位置)。这是为了在遍历的时候可以用迭代器!=head以及tail来判断是否到达边界。

几个list重要操作的原理与实现

现在假设我们已经有了一个功能完整的迭代器,那么接下来的操作基于迭代器就好了。

删除

先给出图和代码:

iterator erase( iterator itr )
{
Node *p = itr.current;
iterator retVal( p->next );
p->prev->next = p->next;
p->next->prev = p->prev;
delete p;
--theSize; return retVal;
} iterator erase( iterator from, iterator to )
{
for( iterator itr = from; itr != to; )
itr = erase( itr ); return to;
}

直观上看我们只要删除1和2号线,增加3和4号线就可以了。代码就是上面的代码哈,很简单。但是,经常容易犯得错误是:忘记先保存删除节点的位置。那么在1与2号线删除之后就再也无法释放其资源了哈,这就是第一行代码 Node *p = itr.current; 的用意。第二行代码 iterator retVal( p->next ); 是为了删除之后仍然能够得到有效的迭代器。

插入

插入操作也是类型的,只不过这里用了一个较为看上去复杂的操作 p->prev = p->prev->next = new Node{ x, p->prev, p } 。记住,c++赋值语句,是从右往左执行的。结合图,也就不难理解这句话了。

// Insert x before itr.
iterator insert( iterator itr, const Object & x )
{
Node *p = itr.current;
++theSize;
return iterator( p->prev = p->prev->next = new Node{ x, p->prev, p } );
} // Insert x before itr.
iterator insert( iterator itr, Object && x )
{
Node *p = itr.current;
++theSize;
return iterator( p->prev = p->prev->next = new Node{ std::move( x ), p->prev, p } );
}

其他--stl/java

stl里list的实现实际上稍有出入,主要体现在内存分配与构造。实在是能力有限,在这里不敢瞎说。推荐大家阅读侯捷老师的《STL源码剖析》。另外java里的迭代器需要实现Iterator接口就好了。

关于queue与stack的基于node的实现就不赘述了。另外dequeue也可基于node实现,但是stl里是基于分段连续线性空间实现的,如果有可能会在后续的博客中详细讲述。

最后给一个我之前写的dequeue-java版本作为结束。

import java.util.Iterator;
import java.util.NoSuchElementException; public class Deque<Item> implements Iterable<Item> {
private Node first = null;
private Node last = null;
private int size = 0; public Deque() { } private class Node{
Item item;
Node next;
Node previous;
} public boolean isEmpty() {
return first==null && last==null;
} public int size() {
return size;
} public void addFirst(Item item) {
if(item==null)
throw new IllegalArgumentException();
if(0==size) {
//if size==0, let first and last point to item
first = new Node();
first.item = item;
last = first;
}else {
Node oldfirst = first;
first = new Node();
first.item = item;
first.next = oldfirst;
oldfirst.previous = first;
}
size++;
} public void addLast(Item item) {
if(item==null)
throw new IllegalArgumentException();
if(0==size) {
//if size==0, let first and last point to item
last = new Node();
last.item = item;
first = last;
}else {
Node oldlast = last;
last = new Node();
last.item = item;
last.previous = oldlast;
oldlast.next = last;
}
size++;
} public Item removeFirst() {
if(size==0)
throw new NoSuchElementException();
Item itemtem = first.item;
if(size==1) {
first = null;
last = null;
}else {
Node oldfirst = first;
first = oldfirst.next;
first.previous = null;
oldfirst.next = null;
oldfirst.item = null;
}
size--;
return itemtem;
} public Item removeLast() {
if(size==0)
throw new NoSuchElementException();
Item itemtem = last.item;
if(size==1) {
itemtem = last.item;
first = null;
last = null;
}else {
Node oldlast = last;
last = oldlast.previous;
last.next = null;
oldlast.previous = null;
oldlast.item = null;
}
size--;
return itemtem;
} private class DequeIterator implements Iterator<Item>{
private Node current = first;
@Override
public boolean hasNext() {
return current != null;
} @Override
public Item next() {
if(!hasNext())
throw new NoSuchElementException();
Item itemtem = current.item;
current = current.next;
return itemtem;
} public void remove() {
throw new UnsupportedOperationException();
}
} public Iterator<Item> iterator(){
return new DequeIterator();
} public static void main(String[] args) {
// TODO Auto-generated method stub
Deque<Integer> deque = new Deque<Integer>();
deque.addFirst(1);
deque.addFirst(2);
deque.addFirst(3);
deque.addLast(4);
deque.addLast(5);
deque.addLast(6);
deque.removeFirst();
deque.removeLast();
deque.removeFirst();
deque.removeLast();
deque.removeFirst();
deque.removeLast();
for (Integer integer : deque) {
System.out.println(integer);
}
} }

See you next time. Happy Coding!!!

我的github

数据结构与算法(4) -- list、queue以及stack的更多相关文章

  1. ASP.NET MVC深入浅出系列(持续更新) ORM系列之Entity FrameWork详解(持续更新) 第十六节:语法总结(3)(C#6.0和C#7.0新语法) 第三节:深度剖析各类数据结构(Array、List、Queue、Stack)及线程安全问题和yeild关键字 各种通讯连接方式 设计模式篇 第十二节: 总结Quartz.Net几种部署模式(IIS、Exe、服务部署【借

    ASP.NET MVC深入浅出系列(持续更新)   一. ASP.NET体系 从事.Net开发以来,最先接触的Web开发框架是Asp.Net WebForm,该框架高度封装,为了隐藏Http的无状态模 ...

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

      本篇是数据结构与算法的第三篇,本篇我们将来了解一下知识点: 队列的抽象数据类型 顺序队列的设计与实现 链式队列的设计与实现 队列应用的简单举例 优先队列的设置与实现双链表实现 队列的抽象数据类型 ...

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

      本篇是数据结构与算法的第三篇,本篇我们将来了解一下知识点: 队列的抽象数据类型 顺序队列的设计与实现 链式队列的设计与实现 队列应用的简单举例 优先队列的设置与实现双链表实现 队列的抽象数据类型 ...

  4. 第三节:深度剖析各类数据结构(Array、List、Queue、Stack)及线程安全问题和yeild关键字

    一. 各类数据结构比较及其线程安全问题 1. Array(数组): 分配在连续内存中,不能随意扩展,数组中数值类型必须是一致的.数组的声明有两种形式:直接定义长度,然后赋值:直接赋值. 缺点:插入数据 ...

  5. 【C#复习总结】探究各类数据结构(Array、List、Queue、Stack)及线程安全问题和yeild关键字

    前言 先普及一下线程安全和类型安全 线程安全: 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码.如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的 ...

  6. .NET面试题系列(五)数据结构(Array、List、Queue、Stack)及线程安全问题

    常用数据结构的时间复杂度 如何选择数据结构 Array (T[]) 当元素的数量是固定的,并且需要使用下标时. Linked list (LinkedList<T>) 当元素需要能够在列表 ...

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

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

  8. 每周一练 之 数据结构与算法(Stack)

    最近公司内部在开始做前端技术的技术分享,每周一个主题的 每周一练,以基础知识为主,感觉挺棒的,跟着团队的大佬们学习和复习一些知识,新人也可以多学习一些知识,也把团队内部学习氛围营造起来. 我接下来会开 ...

  9. 每周一练 之 数据结构与算法(Queue)

    这是第二周的练习题,这里补充下咯,五一节马上就要到了,自己的计划先安排上了,开发一个有趣的玩意儿. 下面是之前分享的链接: 1.每周一练 之 数据结构与算法(Stack) 2.每周一练 之 数据结构与 ...

随机推荐

  1. HDU 1814 Peaceful Commission(2-sat 模板题输出最小字典序解决方式)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1814 Problem Description The Public Peace Commission ...

  2. Android studio一些设置项

    ------Appearance选项------------------------------------------------------------- Cylic scrolling in l ...

  3. android studio中xml文件代码提示问题

    在系统控件中输入“a”能提示出android:id等所有属性.而在第三方库的控件中输入“a”只会提示“appNs”,但如果手动写app:id="@+id/aaa"系统也是可以识别的 ...

  4. Xshell配色方案啊【学习笔记】

    自己移植从putty版本移植到Xshell的配色方案,效果不错,看上去挺舒服. [myisayme] text(bold)=eaeaea magenta(bold)=ff55ff text=fffff ...

  5. 分享tiny4412,emmc烧录u-boot, 支持fastboot模式烧写emmc【转】

    本文转载自:http://www.arm9home.net/read.php?tid-80810.html 分享tiny4412,emmc烧录u-boot, 支持fastboot模式烧写emmc   ...

  6. C# 获得资源文件下图片的路径

    最终实现正确的代码是: button8.Image = System.Drawing.Image.FromFile(@"..\\..\\Resources\\GAOJIBAN.png&quo ...

  7. 05、ListActivity的使用

    第一个好处:处理共同的操作,避免代码重复 假设我要写第二个界面我也是需要使用到mapView,那你都要去查找一个mapView.都要获取一个Map的一个地图. 第二个好处:代码规范(方便阅读,真实开发 ...

  8. cloudstack ---部署的架构

    cloudstack跟KVM一起部署的架构 下图是CloudStack跟kvm一起部署的架构: 在每个kvm的宿主机上都需要部署agent程序. cloudstack跟vsphere一起部署的架构 下 ...

  9. shell脚本-基础

    shell脚本-基础 编程基础 程序是指令+ 数据 程序编程风格: 过程式:以指令为中心,数据服务于指令 对象式:以数据为中心,指令服务于数据 shell 程序提供了编程能力,解释执行. 计算运行二进 ...

  10. codevs1005生日礼物(dfs)

    1005 生日礼物  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 黄金 Gold     题目描述 Description 9月12日是小松的朋友小寒的生日.小松知道小寒特别 ...