博文
数据结构学习(C++)——如何在一个链表中链入不同类型的对象(2007-04-02 06:15:00)
摘要:乎你也注意到了,不管怎么定义,好像一个链表中的对象都是同一类型的。而实际上,这也是必须的,否则,返回节点中的数据这样的函数的返回值的类型是什么呢?但是,人的要求是无止境的……(省略本人感慨若干百字)。把不同的对象链在一个链表中的目的是为了方便使用,现在一定记住这个原则,后面的讨论都是基于这个原则的,否则,我们就是技术狂人了——偏偏实现一些看起来不可能的事情。
达到这个目标的原理其实很简单,只要把不同类型的对象变成同样的类型就可以了。看下面的结构定义:
struct Mobject
{
void *p;
int ObjectType;
};
将一个对象链入链表时,将指向这个对象的指针赋给p,同时记录对象类型。当取得这个节点的时候,根据ObjectType的值来确定p指的对象类型,从而还原指针类型,也就得到了原来的对象。
后面讲到的广义表实际上采用的就是这种方法。显而易见的,这样的Mobject支持的对象是预先确定的,你将自己维护ObjectType列表,每添加一种类型的支持,你需要在ObjectType列表中给出它的替代值,然后在相应的switch(ObjectType)给出这种类型的case语句。很烦人是吧,下面给出另一种方法,其实还是这个原理,不同的是,把这个烦人的工作交给编译器了。
还记得前边强调的原则吗,为什么我们将不同类型的对象放在一个链表中呢?很显然,我们想达到这样的一个效果:比如说,我们在一个链表中储存了三角形,直线,圆等图形的参数,我们希望对某个节点使用Draw()方法,就重绘这个图形;使用Get()则得到这个图形的各个参数;使用Put()则修改图形的参数。可以看出,这些不同的对象实际上有同样的行为,只是实现的方法不同。
C++的多态性正好可以实现我们的构想。关于这方面,请参阅相关的C++书籍(我看的是《C++编程思想》)。请看如下的例子:
#ifndef Shape_H
#define Shape_H
class Shape
{
public:
virtual void ......
数据结构学习(C++)——队列应用(事件驱动模拟)(2007-04-02 06:14:00)
摘要:看的两本教科书(《数据结构(C语言版)》还有这本黄皮书)都是以这个讲解队列应用的,而且都是银行营业模拟(太没新意了)。细比较,这两本书模拟的银行营业的方式还是不同的。1997版的《数据结构(C语言版)》的银行还是老式的营业模式(毕竟是1997年的事了),现在的很多地方还是这种营业模式——几个窗口同时排队。这种方式其实不太合理,经常会出现先来的还没有后来的先办理业务(常常前面一个人磨磨蹭蹭,别的队越来越短,让你恨不得把前面那人干掉)。1999版的这本黄皮书的银行改成了一种挂牌的营业方式,每个来到的顾客发一个号码,如果哪个柜台空闲了,就叫号码最靠前的顾客来办理业务;如果同时几个柜台空闲,就按照一种法则来决定这几个柜台叫号的顺序(最简单的是按柜台号码顺序)。这样,就能保证顾客按照先来后到的顺序接受服务——因为大家排在一个队里。这样的营业模式我在北京的西直门工商银行见过,应该说这是比较合理的一种营业模式。不过,在本文中最重要的是,这样的营业模式比较好模拟(一个队列总比N个队列好操作)。
原书的这部分太难看了,我看的晕晕的,我也不知道按照原书的方法能不能做出来,因为我没看懂(旁白:靠,你小子这样还来现眼)。我按照实际情况模拟,实现如下:
#ifndef Simulation_H
#define Simulation_H
#include <iostream.h>
#include <stdlib.h>
#include <time.h>
class Teller
{
public:
int totalCustomerCount;
int totalServiceTime;
int finishServiceTime;
Teller() :totalCustomerCount(0), totalServiceTime(0),
&nbs......
数据结构学习(C++)——栈应用(表达式求值)(2007-04-02 06:14:00)
摘要:栈的应用很广泛,原书只讲解了表达式求值,那我也就只写这些。其实,栈的最大的用途是解决回溯问题,这也包含了消解递归;而当你用栈解决回溯问题成了习惯的时候,你就很少想到用递归了,比如迷宫求解。另外,人的习惯也是先入为主的,比如树的遍历,从学的那天开始,就是递归算法,虽然书上也教了用栈实现的方法,但应用的时候,你首先想到的还是递归;当然了,如果语言本身不支持递归(如BASIC),那栈就是唯一的选择了——好像现在的高级语言都是支持递归的。
如下是表达式类的定义和实现,表达式可以是中缀表示也可以是后缀表示,用头节点数据域里的type区分,这里有一点说明的是,由于单链表的赋值函数,我原来写的时候没有复制头节点的内容,所以,要是在两个表达式之间赋值,头节点里存的信息就丢了。你可以改写单链表的赋值函数来解决这个隐患,或者你根本不不在两个表达式之间赋值也行。
#ifndef Expression_H
#define Expression_H
#include "List.h"
#include "Stack.h"
#define INFIX 0
#define POSTFIX 1
#define OPND 4
#define OPTR 8
template <class Type> class ExpNode
{
public:
int type;
union { Type opnd; char optr;};
ExpNode() : type(INFIX), optr('=') {}
ExpNode(Type opnd) : type(OPND), opnd(opnd) {}
ExpNode(char optr) : type(OPTR), optr(optr) {}
};
template <cla......
四则运算实现(2007-04-02 06:13:00)
摘要:#include"conio.h"
#include "stdio.h"
#include "stdlib.h"
#define STACK_INIT_SIZE 100
#define STACKINCREMENT 10
typedef char SElemType;
typedef int Status;
#define OK 1
#define OVERFLOW 0
#define ERROR 0
typedef struct{
SElemType *base;
SElemType *top;
int stacksize;
}SqStack;
Status InitStack(SqStack &s){//构造一个空栈
s.base=(SElemType *)malloc(STACK_INIT_SIZE*sizeof(SElemType));
if(!s.base)exit(OVERFLOW);
s.top=s.base;
s.stacksize=STACK_INIT_SIZE;
return OK;
}//InitStack
SElemType Getop(SqStack s){//若栈不空,则返回用e返回s栈元素
SElemType e;
if(s.top==s.base)return ERROR;
e=*(s.top-1);
return e;
}//Getop
Status&nbs......
数据结构学习(C++)——栈和队列(定义和实现)(2007-04-02 06:12:00)
摘要:栈和队列是操作受限的线性表,好像每本讲数据结构的数都是这么说的。有些书按照这个思路给出了定义和实现;但是很遗憾,这本书没有这样做,所以,原书中的做法是重复建设,这或许可以用不是一个人写的这样的理由来开脱。
顺序表示的栈和队列,必须预先分配空间,并且空间大小受限,使用起来限制比较多。而且,由于限定存取位置,顺序表示的随机存取的优点就没有了,所以,链式结构应该是首选。
栈的定义和实现
#ifndef Stack_H
#define Stack_H
#include "List.h"
template <class Type> class Stack : List<Type>//栈类定义
{
public:
void Push(Type value)
{
Insert(value);
}
Type Pop()
{
Type p = *GetNext();
RemoveAfter();
return p;
 ......
数据结构学习(C++)——稀疏矩阵(十字链表【2】)(2007-04-02 06:11:00)
摘要:如果你细想想,就会发现,非零元节点如果没有指示位置的域,那么做加法和乘法时,为了确定节点的位置,每次都要遍历行和列的链表。因此,为了运算效率,这个域是必须的。为了看出十字链表和单链表的差异,我从单链表派生出十字链表,这需要先定义一种新的结构,如下:
class MatNode
{
public:
int data;
int row, col;
union { Node<MatNode> *down; List<MatNode> *downrow; };
};
另外,由于这样的十字链表是由多条单链表拼起来的,为了访问每条单链表的保护成员,要声明十字链表类为单链表类的友元。即在class List的声明中添加friend class Matrix;
稀疏矩阵的定义和实现
#ifndef Matrix_H
#define Matrix_H
#include "List.h"
class MatNode
{
public:
int data;
int row, col;
union { Node<MatNode> *down; List<MatNode> *downrow; };
MatNode(int value = 0, Node<MatNode> *p = NULL, int i = 0, int j = 0)
: data(value), down(p), row(i), ......
数据结构学习(C++)——线性链式结构总结(代后记)【1】(2007-04-02 06:11:00)
摘要:看到这个标题,有些人一定松了一口气——这小子可算白话完了,当然了,你要是略有惋惜之情,我真是受宠若惊。但不论你怎么想,写到这里只是告一段落,并没有完,后面还有很大一部分呢,比如树、图、查找、排序——这么多年了,还是这点东西。代后记的意思是,我觉得对前面线性链式结构的总结,对后面的学习有指导意义:从前面的学习中,你能得出如何学习数据结构,以及如何正确看待这门课——如果你能从重复建设中看到这样做的价值,你才能真正理解这门课的意义。
在开始总结前,先整理一下以前的代码,假定你使用的是VC6,你的工程中现在应该有这几个主要文件:Node.h、List.h、Stack.h、Queue.h、CircList.h、DblList.h、Polynomial.h、Expression.h、Matrix.h,一个含有main()的cpp文件。其他的是一些测试文件和一些应用,比如Simulation.h。如果你用的不是VC6,你一定背地里咒骂我多少次了,因为一大堆error和warning。我当初发布的时候,并没有考虑到不同编译器的差异,我觉得只要光使用标准库(仅仅用了iostream.h和stdlib.h),不写怪怪的代码,通用性应该不是问题,但实际上不是这样。所以,我不得不花一些篇幅介绍如何修改以前的代码,所以,这篇文章就只能是【1】了;不过,后面有一个计时器类的源码,就算是一点补偿吧。
VC6、BCB6、Dev-cpp的编译器的差异
这些应该是目前Win32下最常用的IDE环境了,各自的编译器分别是CL.exe、BCC32.exe、G++.exe(就是GNU C++)。我没装BCB6,所以只是拿BCC32来代替。VC6时间比较早(98年),对C++标准支持不是很完善,例如下面的代码:
for (int i = 1; i < 10; i++); for (int i = 1; i < 10; i++);
按照C++标准,i的作用域应该是for循环内。但是在VC6中,出了定义i的for循环,仍然有效,所以,这段代码在VC6中被认为是重复定义。我为了省事,在并列的第二个循环内,把int省略了。现在到了BCC32,它在这方面对于C++标准倒是很支持,于是,象我那样的做法,就是第二个i没定义。让这样的代码同时适应两个编译器的解决办法,要么第2个循环变量换个名字,要么退回......
数据结构学习(C++)——图【1】(基本储存方法)(2007-04-02 06:10:00)
摘要:首先告诉大家一个好消息,数据结构到这里就要结束了!然后再来一个坏消息,这里是数据结构中“最没有意义”的部分和最难的部分。图的应用恐怕是所有结构中最宽泛的了,但这也注定了在讲“数据结构的图”的时候没什么好讲的——关于图的最重要的是算法,而且相当的一部分都是很专业的,一般的人几乎不会接触到;相对而言,结构就显得分量很轻。你可以看到关于图中元素的操作很少,远没有单链表那里列出的一大堆“接口”。——一个结构如果复杂,那么能确切定义的操作就很有限。
不管怎么说,还是先得把图存起来。不要看书上列出了好多方法,根本只有一个——邻接矩阵。如果矩阵是稀疏的,那就可以用十字链表来储存矩阵(见前面的《稀疏矩阵(十字链表)》)。如果我们只关系行的关系,那么就是邻接表(出边表);反之,只关心列的关系,就是逆邻接表(入边表)。
下面给出两种储存方法的实现。
#ifndef Graphmem_H
#define Graphmem_H
#include <vector>
#include <list>
using namespace std;
template <class name, class dist, class mem> class Network;
const int maxV = 20;//最大节点数
template <class name, class dist>
class AdjMatrix
{
friend class Network<name, dist, AdjMatrix<name, dist> >;
public:
AdjMatrix() : vNum(0), eNum(0)
{
vertex = new name[maxV]; edge = new dist*[maxV];
......
数据结构学习(C++)——图【2】(DFS和BFS(2007-04-02 06:09:00)
摘要:对于非线性的结构,遍历都会首先成为一个问题。和二叉树的遍历一样,图也有深度优先搜索(DFS)和广度优先搜索(BFS)两种。不同的是,图中每个顶点没有了祖先和子孙的关系,因此,前序、中序、后序不再有意义了。仿照二叉树的遍历,很容易就能完成DFS和BFS,只是要注意图中可能有回路,因此,必须对访问过的顶点做标记。
最基本的有向带权网
#ifndef Graph_H
#define Graph_H
#include <iostream>
#include <queue>
using namespace std;
#include "Graphmem.h"
template <class name, class dist, class mem>
class Network
{
public:
Network() {}
Network(dist maxdist) { data.NoEdge = maxdist; }
~Network() {}
bool insertV(name v) { return data.insertV(v); }
bool insertE(name v1, name v2, dist cost) { return data.insertE(v1, v2, cost); }
name& getV(int n) { return data.getV(n); }
int nextV(int m, int n = -1) { return data.nextV(m, n); }
......
数据结构学习(C++)——树(总结)(2007-04-02 06:08:00)
摘要:才刚开了个头,就要说再见了——在树这里,除了二叉树,别的都还没有讲。为什么可以总结了呢?因为前面已经涉及到了树的两个基本用途,而如果再讲B+、B-,就不能不提到搜索,如果是胜者树就不能不提到排序。为此,把这部分放到后面。我前面所做的努力,只是让你有个基本概念,什么时候记得用树。
树的两个基本用途,可以用物质和精神来比喻。
一个用途是做为数据储存,储存具有树结构的数据——目录、族谱等等。为了在实际上是线性的储存载体上(内存、磁盘)储存非线性的树结构,必须有标志指示出树的结构。因此,只要能区分根和子树,树可以采取各种方法来储存——多叉链表、左子女-右兄弟二叉链表、广义表、多维数组。由于操作的需求,储存方法并不是随意选取的。比如,在并查集的实现上,选取的是双亲链表。
一个用途是做为逻辑判断,此时会常常听到一个名词——判定树。最常用的结构是二叉树,一个孩子代表true,一个孩子代表false。关于多叉判定树,有个例子是求8皇后的全部解——这个连高斯都算错了(一共是92组解,高斯最开始说76组解),我们比不上高斯,但是我们会让computer代劳。
就像哲学界到现在还纠缠于物质和精神本源问题,实际上在树这里也是如此。有些情况下,并不能区分是做为储存来用还是做为判断来用,比如搜索树,既储存了数据,还蕴涵着判断。
和后面的图相比,树更基本,也更常用。你可以不知道最短路径怎么求,却每时每刻都在和树打交道——看看你电脑里的文件夹吧。
最后,附带一个求N皇后的全部解的程序。
#include <stdio.h>
#define N 8
int layout[N];//布局
int key = 0;
int judge(int row, int col)//判断能否在(row,col)放下
{
int i;
for (i = 0; i < row; i++)
{
i......
