C递归实现单向链表的反转
众所周知,链表作为线性表的一种实现方式,有插入删除方便的优点,但不能对节点进行随机访问.同时,要想知道某一节点的前驱节点,必须从头节点开始遍历才能找到,这体现了单链表的方向性.由于单链表方向性的存在,使得链表的反转变得复杂起来.
在这里,采用递归的算法,可以使反转过程看起来更容易理解,代码如下:
1 /*
2 Code by : EricYou
3 http://www.cnblogs.com/yxin1322
4 Date: 2006.1.14
5
6 */
7
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <conio.h>
11
12 /*链表节点存储的数据*/
13 typedef char ElemType;
14
15
16 /*链表节点结构*/
17 typedef struct LinkListNode
18 {
19 ElemType data;
20 struct LinkListNode * next;
21 } *LinkList,*pNode,LinkListNode;
22
23
24 /*递归实现反转不带头节点的单向链表
25
26 pNode:PerNode 前一节点指针
27 pNode:CurrentNode 后一节点指针
28 visit 自定义节点访问函数
29 */
30 LinkList ReverseLinkList( pNode PreNode, pNode CurrentNode, void(*visit)(ElemType e) )
31 {
32 if(!CurrentNode) /*如果当前节点指针为空,则返回NULL,此种情况只有在*/
33 return NULL; /*链表头指针为空时才可能执行到*/
34
35 if(!CurrentNode->next) /*如果下一节点指针为空,则将当前节点next指针指向上一节点,并*/
36 { /*返回当前节点指针,次种情况只有在当前节点为尾节点时才能执行到*/
37 visit(CurrentNode->data);
38 CurrentNode->next=PreNode;
39 return CurrentNode;
40 }
41 else /*其他节点的情况,先调用递归函数反转后续节点,再反转当前节点*/
42 { /*并将递归函数返回来的尾节点指针向上层函数返回*/
43 LinkList FirstNode;
44
45 FirstNode=ReverseLinkList(CurrentNode,CurrentNode->next,visit);
46
47 visit(CurrentNode->data);
48 CurrentNode->next=PreNode;
49 return FirstNode; /*返回的是尾节点的指针,即反转后新链表的头指针*/
50 }
51 }
52
53 /*自定义链表节点访问函数*/
54 void visit(ElemType e)
55 {
56 printf("process node %c\n",e);
57 }
58
59
60 /*打印链表*/
61 void PrintLinkList(LinkList PL)
62 {
63 printf("\n--------------------------\n");
64 while(PL)
65 {
66 printf("%c ",PL->data);
67 PL=PL->next;
68 }
69 printf("\n--------------------------\n");
70 }
71
72 /*用键盘输入初始化一个链表,参数为链表头指针地址*/
73 void InitializeLinkList(LinkList * Head)
74 {
75 pNode EndNode=*Head; /*总是指向链表最后一个节点*/
76
77 pNode TempNode=NULL; /*临时节点,存放当前输入的链表节点*/
78 int i=1; /*节点记数*/
79 char getEnter;
80
81 printf("please input characters of the linklist(end with Enter):\n");
82
83 /*开始构造不带头节点的单向链表*/
84 while(1)/*不停地循环接受字符*/
85 {
86 TempNode=(pNode)malloc(sizeof(LinkListNode)); /*为当前输入的链表节点分配空间*/
87 TempNode->next=NULL;
88
89 printf("Node[%d]:",i);
90 TempNode->data=getchar(); /*接受字符*/
91
92
93 if(TempNode->data== ' \n ' ) /*如果直接回车,则结束链表的输入*/
94 {
95 TempNode=NULL;
96 break;
97 }
98 getEnter=getchar(); /*接受输完字符后的回车*/
99
100 if(i==1) /*如果当前输入的是第一个节点,则让头指针指向它*/
101 {
102 *Head=TempNode;
103 EndNode=*Head;
104 }
105
106 EndNode->next=TempNode; /*将当前输入的节点接到链表尾端*/
107 EndNode=TempNode;
108
109 i++;
110 }
111 }
112
113 int main()
114 {
115 LinkList head=NULL; /*声明头指针*/
116
117 InitializeLinkList(&head); /*初始化链表*/
118
119 PrintLinkList(head); /*打印原链表*/
120
121 head=ReverseLinkList(NULL,head,visit); /*调用反转函数*/
122
123 PrintLinkList(head); /*打印反转后的链表*/
124
125 return 1;
126 }
巧用二叉树原理求解集合的幂集
幂集是《离散数学》集合论中一个重要的概念,集合在《离散数学》中定义如下:集合是一个元概念,它研究对象的全体,通常用大写字母表示集合。如集合A,集合B。集合的表示方式有两种,分别是例举法和描述法。
如一个由大于1小于5的整数组成的集合,用例举法可以表示为:
设该集合为A,则 A={2,3,4 }
用描述法可表示为:
设该集合为A,则A={x | 1<x<5 , x∈Z}
集合的子集定义为:若对于一个集合B,它的所有元素都同时属于A,那么B是A的子集,特别的,如果一个集合没有任何元素,那么我们称它为空集,空集是任何集合的子集。同时,一个集合本身也是自己的子集。
现在我们终于可以引出幂集的概念了,幂集被定义为:一个集合的所有子集组成的集合成为该集合的幂集。由此可见幂集是集合的集合。如集合A={1,2,3},那么它的幂集为P(A)={ {},{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3} },一个有n个元素的集合,它的幂集有2n个元素,这些元素都是该集合的子集。
通过上面的简单介绍,我们看到,要求一个集合的幂集,在该集合所含元素较少时是可以很快看出的,但当集合元素增多时,幂集的元素则成几何级数增长,求集合的幂集变得非常困难。
在这里,我编制了一个求集合幂集的程序,通过遍历一棵满二叉树,求解集合的幂集。程序的原理是:把求幂集元素的过程看作是在先序遍历一棵深度为n+1的满二叉树,从根节点开始,访问左孩子表示幂集元素(集合的子集)中不包含集合的第一个元素,访问右孩子表示幂集元素中包含集合的第一个元素,这样,在二叉树的第二层完成了对集合第一个元素的取舍,依次类推,当遍历到达第n+1层,也就是二叉树的叶子节点时,完成了集合所有元素的取舍,这时输出一个取舍后的幂集元素。满二叉树的第n+1层共有2n个叶子节点,代表了集合的2n个幂集元素,待遍历输出完整棵满二叉树的叶子节点,也就得到了我们要求的幂集。
程序实现如下:
1 /*
2 code by: EricYou
3 date: 2006.1.7
4 blog: http://www.cnblogs.com/yxin1322
5
6 */
7
8 #include <stdio.h>
9 #include <string.h>
10
11 #define MAX_LENGTH 100 /*集合的最大元素个数*/
12
13 void PowerSet(char*, int, char*,int *);
14
15 int main()
16 {
17 char a[MAX_LENGTH]; /*存储输入的集合*/
18 char set[MAX_LENGTH]={"\0"}; /*储存集合的幂集元素*/
19 int NumOfPowerSet=0; /*幂集元素记数*/
20
21 printf("Input the elements:");
22 scanf("%s",a);
23
24 printf("----------------------------\n");
25 PowerSet(a,0,set,&NumOfPowerSet); /*调用递归函数*/
26 printf("----------------------------\n");
27
28 printf("Number of PowerSet: %d\n",NumOfPowerSet);
29
30 return 1;
31 }
32
33 /*
34 参数说明: char* a : 待求幂集的集合
35 int i : 当前分析到集合的第i个元素
36 char* set : 存储当前幂集元素状态
37 int* Num : 幂集元素记数
38 */
39 void PowerSet(char* a, int i, char* set, int * Num)
40 {
41 char TempSet[MAX_LENGTH];
42
43 strcpy(TempSet,set);
44 if(i>=strlen(a))
45 {
46 printf("{%s}\n",set);
47 (*Num)++;
48 }
49 else
50 {
51 PowerSet(a,i+1,TempSet,Num);
52 strncat(TempSet,(a+i),1);
53 PowerSet(a,i+1,TempSet,Num);
54 }
55 }
输入测试数据:ABCD,表示集合{A,B,C,D},得到输出数据如下:
Input the elements:ABCD
-------------------------
{}
{D}
{C}
{CD}
{B}
{BD}
{BC}
{BCD}
{A}
{AD}
{AC}
{ACD}
{AB}
{ABD}
{ABC}
{ABCD}
-------------------------
Number of PowerSet: 16
动态链接库(Dynamic Link Library)学习笔记
作者:EricYou 转载请注明出处
注:本文所写的动态链接库指传统的DLL,并非是.NET中的
Assembly.
我对动态链接和动态链接库的概念并不陌,但一直以来就停留在概念的层面上,没有更深入的了解。今天抽空看了一下有关动态链接和动态链接库的文章,有了一些新的认识,当然不能忘了写在这里。那么现在就开始...
什么是动态链接和动态链接库
动态链接(Dynamic Linking)是相对于静态链接(Static Linking)而言的。程序设计中,为了能做到代码和模块的重用,程序设计者常常将常用的功能函数做成库,当程序需要实现某种功能时,就直接调用库文件中的函数,从而实现了代码的重用。早期的程序设计中,可重用的函数模块以编译好的二进制代码形式放于静态库文件中,在MS的操作系统中是Lib为后缀的文件。程序编写时,如果用户程序调用到了静态库文件中的函数,则在程序编译时,编译器会自动将相关函数的二进制代码从静态库文件中复制到用户目标程序,与目标程序一起编译成可执行文件。这样做的确在编码阶段实现了代码的重用,减轻了程序设计者的负担,但并未在执行期实现重用。如一个程序a.exe使用了静态库中的 f() 函数,那么当a.exe有多个实例运行时,内存中实际上存在了多份f()的拷贝,造成了内存的浪费。
随着技术的进步,出现了新的链接方式,即动态链接,从根本上解决了静态链接方式带来的问题。动态链接的处理方式与静态链接很相似,同样是将可重用代码放在一个单独的库文件中(在MS的操作系统中是以dll为后缀的文件,Linux下也有动态链接库,被称为Shared Object的so文件),所不同的是编译器在编译调用了动态链接库的程序时并不将库文件中的函数执行体复制到可执行文件中,而是只在可执行文件中保留一个函数调用的标记。当程序运行时,才由操作系统将动态链接库文件一并加载入内存,并映射到程序的地址空间中,这样就保证了程序能够正常调用到库文件中的函数。同时操作系统保证当程序有多个实例运行时,动态链接库也只有一份拷贝在内存中,也就是说动态链接库是在运行期共享的。
使用动态链接方式带来了几大好处:首先是动态链接库和用户程序可以分开编写,这里的分开即可以指时间和空间的分开,也可以指开发语言的分开,这样就降低了程序的耦合度;其次由于动态链接独特的编译方式和运行方式,使得目标程序本身体积比静态链接时小,同时运行期又是共享动态链库,所以节省了磁盘存储空间和运行内存空间;最后一个是增加了程序的灵活性,可以实现诸如插件机制等功能。用过winamp的人都知道,它的很多功能都是以插件的形式提供的,这些插件就是一些动态链接库,主程序事先规定好了调用接口,只要是按照规定的调用接口写的插件,都能被winamp调用。
WIndow 95、98、NT系列等系统都提供了动态链接库的功能,并且这些操作系统的系统调用大多都是通过动态链接库实现的,最常见的NT系列OS中的KENEL32.dll,USER32.dll,GDI32.dll等动态链接库文件就包含了大量的系统调用。在windows家族中,NT内核的操作系统在动态链接库机制上较之前的95、98系统要更安全。95、98系统在程序调用动态链接库时,将动态链接库加载到2G-3G之间的被称为进程共享空间的虚拟地址空间,并且所有进程关于这1G的虚拟地址空间的页表都是相同的,也就是说对于所有的进程,这片共享区的页表都指向同一组物理页,这样一来,加载入内存的的动态链接库对所有正在运行的进程都是可见的。如果一个动态链接库被其中一个进程更改,或其自身崩溃,将影响到所有调用它的进程,如果该动态链接库是系统的动态链接库,那么将导致系统的崩溃。在Windows NT系统中,动态链接库被映射到进程的用户地址空间中,并用Copy On Write机制保证动态链接库的共享安全,Copy On Write可以理解为写时拷贝。一般情况下,多个运行的进程还是按原来的模式共享同一个动态链接库,直到有进程需要向动态链接库的某个页面写数据时,系统将该页做一个拷贝,并将新复制页面的属性置为可读可写,最后修改进程的页表使之指向新拷贝的物理页。这样无论该进程怎么修改此页的数据,也不会影响到其他调用了此动态链接库的进程了。
Windows下动态链接库的编写
因为本人对linux没有太多研究,所以这里只介绍windwos环境下动态链接库的编写。
在VC中新建一个空的Win32动态链接库工程(Win32 Domanic Library),然后添加一个C++ Sourse File到工程,我这里的文件名取DllTest.cpp。然后在文件中添加如下内容:
//DllTest.cpp
_declspec(dllexport) int add(int a,int b)
{
return a+b;
}
_declspec(dllexport) int subtract(int a,int b)
{
return a-b;
}
接下来编译链接,就会在debug目录下生成一个调试版本的动态链接库,该链接库包含了add和subtract两个可供外部调用的函数。我们注意到,在源文件中多了一个没有见过的语句 _declspec(dllexport) ,这个语句的作用就是向编译器指出我需要在生成的动态链接库中导出的函数,没有导出的函数是不能被其他程序调用的。要知道一个动态链接库导出了什么函数,可以在命令提示行用命令"dumpbin -exports DllTest.dll"来查看(也可以用VC工具包中的depends使用程序来查看)。以下是用dumpbin命令查看DllTest.dll而生成的信息:
Dump of file DllTest.dll
File Type: DLL
Section contains the following exports for DllTest.dll
0 characteristics
4420BEA4 time date stamp Wed Mar 22 11:04:04 2006
0.00 version
1 ordinal base
2 number of functions
2 number of names
ordinal hint RVA name
1 0 0000100A ?add@@YAHHH@Z
2 1 00001005 ?subtract@@YAHHH@Z
Summary
7000 .data
1000 .idata
3000 .rdata
2000 .reloc
2A000 .text
可以看到,我们编写的动态链接库导出了两个函数,分别名为?add@@YAHHH@Z 和 ?subtract@@YAHHH@Z,为什么名字不是add和subtract呢?这是因为C++为了支持函数的重载,会在编译时将函数的参数类型信息以及返回值类型信息加入到函数名中,这样代码中名字一样的重载函数,在经过编译后就互相区分开了,调用时函数名也经过同样的处理,就能找到对应的函数了。编译器对函数的重命名规则是与调用方式相关的,在这里采用的是C++的默认调用方式。以此对应的还有stdcall方式、cdecl方式、fastcall方式和thiscall方式,不同调用方式的重命名规则不一样。
需要特别说一下的是stdcall方式和cdecl方式:
stdcall方式(标准调用方式)也即pascal调用方式,它的重命名规则是函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数所占字节数,之所以要跟参数字节数,是因为stdcall采用被调函数平衡堆栈方式,用函数名最后的数字告诉编译器需要为函数平衡的字节数。例如,如果我们的DllTest.dll采用stdcall方式编译的话,导出的函数名将会是 _add@8 和 _subtract@8 ,而函数编译后的汇编代码最后一句一定是 ret8。
cdecl方式即C语言调用方式,它的重命名规则仅仅是在函数名前加下划线(奇怪的是我用vc6编译的c语言函数,名字没有任何改变),因为C语言采用的是调用函数平衡堆栈的方式,所以不需要在函数名中加入参数所占的字节数,这样的堆栈平衡方式也使C语言可以编写出参数不固定的函数;同时C语言不支持函数重载,因此不需要在函数名中加入参数类型信息和返回值类型信息。
动态链接库已经生成了,接下来就是调用的工作了。调用动态链接库有两种方式:隐式调用和显式调用,下面我们分别来看两种调用方式的具体过程:
动态链接库的隐式调用
新建一个空的Win32 Console Application,命名为DllCaller,向工程中添加名为DllCaller.cpp 的C++ Sourse File,在文件中写入如下代码:
#include <iostream>
using namespace std;
//extern int add(int a,int b);
_declspec(dllimport) int add(int a,int b);
int main()
{
cout<<"3+5="<<add(3,5)<<endl;
return 1;
}
编译,没有错误,链接,有两个错误:找不到外部引用符号。要怎样才能让我们的程序找到动态连接库中的函数呢?这里是关键的一步。到刚才的DllTest工程目录下,从debug文件夹中拷贝生成的DllTest.dll文件和DllTest.lib文件到DllCaller工程目录。然后依次在vc中选择菜单:Project -->Settings-->Liink, 在Object/library Modules中加入一项文件名:DllTest.lib,这里的DllTest.lib并不是静态库文件,而是DllTest.dll的导入库文件,它包含了DllTest.dll动态链接库导出的函数信息,只有在工程链接设置里添加了该文件,才能够使调用了该动态链接库的工程正确链接。完成以上步骤后,我们再编译链接工程,这次没有任何错误!程序可以顺利调用动态连接库文件,正常运行了(为了能使程序找到并加载需要的动态链接库,动态链接库文件必须与调用程序在同一个目录下,或在path环境变量指定的目录下)。
这里需要说明一点,工程中的源文件在调用动态链接库中的函数时,需要提前声明,声名有两种方式,一种是传统的extern方式,一种是_declspec(dllimport)方式,这两种方式在代码中我都给出了。其中,第二种方式能使编译过程更快,所以推荐使用。
动态链接库的显式调用
比起隐式调用,显示调用更加灵活,而且在编译链接时不需要lib导入库文件,也不需要提前声明函数。我们通过windows提供的API函数来动态加载动态连接库并调用其中的函数,用完后可以马上释放内存中的动态链接库,十分方便。下面就是显示调用动态链接库的代码:
#include <iostream>
#include <windows.h>
using namespace std;
int main()
{
HINSTANCE hInstance=LoadLibrary("DllTest.dll");
typedef int (*AddProc)(int,int);
AddProc Add=(AddProc)GetProcAddress(hInstance,?add@@YAHHH@Z);
if(!Add)
{
cout<<"动态连接库库函数未找到"<<endl;
return 0;
}
cout<<"3+5="<<Add(3,5)<<endl;
FreeLibrary(hInstance);
return 1;
}
以上代码并不复杂,首先定义一个实例句柄用来引用由Windows API 函数LoadLibrary加载的动态链接库,LoadLibrary函数的参数是一个字符串指针,具体调用时我们需要填入需要加载的动态链接库的位置及文件名,加载成功后返回一个实例句柄。接下来我们定义一个函数指针类型,用该类型声明一个函数指针,用来存储GetProcAddress函数返回的动态库函数入口地址。GetProcAddress能从指定的动态库中查找指定名字的函数,如果查找成功则返回该函数的入口地址,如果失败则返回NULL。更多GetProcAddress函数的用法请参看MSDN。有人可能注意到,GetProcAddress函数中指定的函数名并不是add,而是?add@@YAHHH@Z。这里就和前面将的函数调用方式联系起来了,在GetProcAddress函数中,我们指定的函数名必须是编译后经过重命名的函数名,而不是源文件中定义的函数名。这样实际上给我们的调用带来了相当大的麻烦,因为我们不可能去了解每一个经过重命名的导出函数名。好在微软已经给出了解决方法,那就是在编写动态链接库时同时编写一个以def为后缀的编译命名参考文件,如果动态链接库工程中有该文件,则编译器会根据该文件指定的函数名来导出动态库函数,关于def文件的详细使用方法请参考MSDN,这里就不一一赘述。找到需要的动态库函数后,我们就可以按需要对它进行调用,之后调用FreeLibrary函数释放动态库。因为动态库是多进程共享的,因此调用FreeLibrary函数并不意味着动态库在内存中被释放,每个动态库都有一个变量用来记录它的共享引用计数,而FreeLibrary的功能只是将这个记数减一,只有当一个动态库的引用计数为0时,它才会被操作系统释放。
隐式调用与显式调用的对比
前面已经详细介绍了动态链接库的两种调用方法,相比之下,隐式调用在编程时比较简单,指定导入库文件后,不必考虑函数的重命名,就可以直接调用动态库函数。但由于隐式调用不能指定动态库的加载时机,因此在一个程序开始运行时,操作系统会将该程序需要的动态链接库都加载入内存,势必造成程序初始化的时间过长,影响用户体验。而显式调用采用动态加载的方法,用到什么加载什么,用完即释放,灵活性较高,可以使程序得到优化。具体运用中到底采用哪种方法,还要依实际情况而定。
若我们的程序中频繁地使用到某些数据和对象,而这些数据和对象的获取和初始化需要消耗大量的时间或服务器性能,这时我们就要考虑对它们进行缓存,以使程序的执行更高效。例如从磁盘读取xml配置文件,若程序中需要频繁地使用配置文件中的配置信息,每次使用时都直接从磁盘读取,频繁的IO操作必然带来程序性能的下降,这时我们需要使用缓存;又如从Web Service获取天气预报数据用于网页显示,若每次显示时都直接调用Web Service获取,频繁的远程调用也会带来可观的性能下降,这时我们仍然需要使用缓存来解决问题。
一般情况下,若数据在一段时间内不会发生变化或变化较小,为了隔离获取数据给程序带来的性能问题,我们可以考虑使用静态缓存。静态缓存在程序的整个生命周期内不会改变缓存的数据,即缓存的数据是稳定的,缓存不对数据进行刷新和同步。Asp.net中,我们可以用System.Web.HttpContext.Current.Cache,Application对象,类静态成员来实现静态缓存。之前例举的使用xml配置文件的例子就可以使用静态缓存来实现。
有时候,我们使用的数据在发布端是经常变化的,而使用了缓存的展现端被要求及时地反应这种变化。例如前面例举的天气预报的例子,天气预报信息在Web Service端会每天甚至每几小时被更新,而使用了缓存的网页端需要及时地显示最新的天气预报信息。这就要求缓存能适时地同步这种变化,我们可以考虑使用动态缓存。动态缓存顾名思义,其中缓存的数据在程序的生命周期内是动态变化的,即缓存会按照某种策略对数据进行更新。在具体实现上,我们可以在静态缓存的基础上,添加一个后台线程对数据进行更新,从而实现简单的动态缓存;也可以使用第三方提供的具有动态刷新能力的缓存模块,如微软企业库的缓存控制块等等。
和DUDU申请了这个帐号快一周了,还没开始写东西,实在是惭愧。平时经常上博客园,看各位大侠的技术文章,也经常以游客的身份参与讨论,其实我早已把博客园当成了自己吸纳新知识的乐园。这里有一群技术扎实又不吝于分享经验的大牛,实在是难得的交流环境,所以索性注册了这个帐号,希望自己也能写点东西,分享自己的所感所思所得。希望自己在不断的写作与交流中能有所收获,不再虚度又一年的光阴。