1. 检索
- 顺序检索。逐个查看每个数据元素是不是要找的那一个。顺序检索非常简单,但是它的工作量与被检索数据的数目成正比。这种检索也成为线性检索。
- 二分检索。做检索的表格本身必须是排好序的,程序还必须知道表格的长度。算法复杂度:log(n)。
// lookup: binary search for value in arr; returen it's index
int lookup( int arr[], int length, int val)
{
int index = - 1 ;
int low = 0 ;
int high = length - 1 ;
int mid;
while (low <= high)
{
mid = (low + high) / 2 ;
if (val < arr[mid])
{
high = mid - 1 ;
}
else if ( val > arr[mid])
{
low = mid + 1 ;
}
else
{
index = mid;
break ;
}
}
return index;
}
2. 排序
- 最好的排序算法之一是快速排序(quicksort),其工作方式就是在数组中划分出小的和大的元素:
从数组中取出一个元素(基准值)。
把其他元素分为两组:
“小的” 是那些小于基准值的元素;
“大的” 是那些大于基准值的元素。
递归地对这两个组做排序。
快排的一种简单实现如下:
#include "stdafx.h"
#include<iostream>
using namespace std;
// Macros, get the length of array
#define NELEMS(arr) ( sizeof(arr)/sizeof(arr[0]) )
// swap: interchange v[i] and v[j]
void swap (int v[], int i, int j)
{
int temp;
if(i != j)
{
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
}
// quicksort: sort v[0] ..v[n-1] into increasing order
void quicksort (int v[], int n)
{
int i, last;
if(n <= 1)
{
return; // nothing to do
}
swap(v, 0, rand() % n);
last = 0;
for ( i = 1; i < n; i++)
{
if(v[i] < v[0])
{
swap(v, ++last, i);
}
}
swap(v, 0, last);
quicksort(v, last);
quicksort(v + last + 1, n - last - 1);
}
int _tmain(int argc, _TCHAR* argv[])
{
int i;
int testArr[] = {6,9,3,10,2,35,0,1};
int length = NELEMS(testArr);
quicksort(testArr, length);
for(i = 0; i < length; i++)
{
cout << "testArr[" << i << "] : " << testArr[i] << endl;
}
return 0;
}
快排的算法复杂度: nlogn。但是,如果执行中经常出现不平均的划分,算法的运行时间可能接近于按 n^2增长。如果数组里所有的值都一样,就会使算法的运行时间达到与n^2成比例。
更多排序见 排序小结。
3. 库
C和C++的标准库里已经包含了排序函数。
- C函数库(stdlib.h)中排序函数是qsort,在调用qsort时必须为它提供一个比较函数,以为在排序中需要比较两个值。由于这里的值可以是任何类型,所以参数是两个void*指针。比较函数传递的是数组元素的地址。
qsort以数组、数组长度、元素的大小以及比较函数作为参数,调用qsort:
qsort(str, N, sizeof (str[ 0 ]), scmp);
eg:
{
int v1, v2;
v1 = * ( int * ) p1;
v2 = * ( int * ) p2;
if (v1 > v2)
return 1 ;
else if (v1 == v2)
return 0 ;
else
return - 1 ;
}
int _tmain( int argc, _TCHAR * argv[])
{
int cqsortArr[ 8 ] = { 10 , 1 , 7 , 5 , 8 , 4 , 30 , 0 };
qsort(cqsortArr, NELEMS(cqsortArr), sizeof (cqsortArr[ 0 ]), icmp); // #define NELEMS(arr) ( sizeof(arr)/sizeof(arr[0]) )
return 0 ;
}
- 标准C++库(algorithm)里有一个名字为sort的类属算法,保证O(nlogn)的执行性质。使用非常简单,不需要强制转换,也不需要知道元素的大小。对于已知的类型,甚至不要求比较函数。
eg:
sort(cppsortArr, cppsortArr + NELEMS(cppsortArr));
4. 一个Java快速排序
Java和C、C++有一个重要的差别:在这里不能把比较函数传递给另一个函数,因为这里没有函数指针。作为替代品,需要建立一个界面创建一个接口,其中仅有的内容就是一个函数,它完成两个Object的比较。这种方法有个限制,只能对所有由Object导出的并带有该机制的类型做排序工作,所以无法用到各种基本类型,如int或double上。对应实现如下:
interface Cmp
{
int cmp(Object x, Object y);
}
//Icmp: Integer comparision
class Icmp implements Cmp
{
public int cmp(Object o1, Object o2)
{
int i1 = ((Integer)o1).intValue();
int i2 = ((Integer)o2).intValue();
if(i1 < i2)
return -1;
else if(i1 == i2)
return 0;
else
return 1;
}
}
class Quicksort
{
static Random rgen = new Random();
// sort: quicksort v[left]..v[right]
static void sort(Object[] v, int left, int right, Cmp cmp)
{
int i, last;
if(left >= right) // nothing to do
return;
swap(v, left, rand(left, right)); // move pivot elem
last = left; // to v[left]
for(i = left + 1; i <= right; i++) //partition
{
if(cmp.cmp(v[i], v[left]) < 0)
swap(v, ++last, i);
}
swap(v, left, last); // restore pivot elem
sort(v, left, last - 1, cmp); // recursively sort
sort(v, last + 1, right, cmp); // each part
}
// swap: swap v[i] and v[j]
static void swap(Object[] v, int i, int j)
{
Object temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
// rand: returen random integer in [left, right] including left and right.
static int rand(int left, int right)
{
return left + Math.abs(rgen.nextInt())%(right - left + 1);
}
}
class TestQuickSort
{
public static void main(String[] args)
{
int i;
int[] arr = {10,2,5,3,11,9,0};
Integer[] integerArr = new Integer[arr.length];
for (i = 0; i < integerArr.length; i++)
{
integerArr[i] = new Integer(arr[i]);
}
Quicksort.sort(integerArr, 0, integerArr.length - 1, new Icmp());
for (i = 0; i < integerArr.length; i++)
{
System.out.println(integerArr[i]);
}
}
}
对应的C#实现如下:
public interface IComp
{
int cmp(object o1, object o2);
}
public class Int32Comp : IComp
{
public Int32 cmp(object o1, object o2)
{
Int32 i1 = Convert.ToInt32(o1);
Int32 i2 = Convert.ToInt32(o2);
if (i1 < i2)
{
return -1;
}
else if (i1 == i2)
{
return 0;
}
else
{
return 1;
}
}
}
public class Quicksort
{
private static Random rd = new Random();
public static void Sort<T>(T[] v, int left, int right, IComp cmp)
{
int i, last;
if (left >= right)
return;
Swap<T>(v, left, rd.Next(left, right + 1));
last = left;
for (i = left + 1; i < right + 1; i++)
{
if (cmp.cmp(v[i], v[left]) < 0)
{
Swap(v, ++last, i);
}
}
Swap<T>(v, left, last);
Sort<T>(v, left, last - 1, cmp);
Sort<T>(v, last + 1, right, cmp);
}
public static void Swap<T>(T[] v, int i, int j)
{
T temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
}
class Program
{
static void Main(string[] args)
{
Int32[] arr = { 101, 11, 2, 7, 4, 5, 3, 30 };
Quicksort.Sort(arr, 0, arr.Length - 1, new Int32Comp());
foreach (Int32 i in arr)
{
Console.WriteLine(i);
}
}
}
5. 大O记法
我们常需要描述特定算法?相对于n(输入元素的个数)需要做的工作量。人们提供一中标准的记法,成为“大O记法”。在描述中使用的基本参数是n,即问题的规模,把复杂性或运行时间表达为n的函数。这里的复杂性应该看作是算法的时间或空间开销的一种抽象。
我们还应该区分算法的最坏情况的行为和期望行为。例如,快速排序的最坏情况运行时间是O(n^2),但期望时间是O(nlogn)。
下面是些常见算法的时间复杂度:
记法 名字 例子
O(1) 常数 下标数组访问
O(logn) 对数 二分检索
O(n) 线性 字符串比较
O(nlogn) nlogn 快速排序
O(n^2) 平方 简单排序算法
O(n^3) 立方 矩阵乘法
O(2^n) 指数 集合划分
6. 可增长数组
我们常常需要记录一个包含某些东西的变量的变化情况,对此数组仍然是一种选择。为减小重新分配的代价,数组应当以成块的方式重新确定大小。C++和Java标准库里可以找到具有这种性质的类,在C中可以通过struct实现。
typedef struct Nameval Nameval;
struct Nameval
{
char * name;
int value;
};
struct NVtab
{
int nval; /* current number of values */
int max; /* allocated number of values */
Nameval * nameval; /* array of name-value pairs */
}nvtab;
enum {NVINIT = 1, NVGROW = 2};
/* addname: add new name and value to nvtab */
int addname(Nameval newname)
{
Nameval * nvp;
if(nvtab.nameval == NULL) // first time
{
nvtab.nameval = (Nameval *) malloc(NVINIT * sizeof(Nameval));
if(nvtab.nameval == NULL)
{
return -1;
}
nvtab.max = NVINIT;
nvtab.nval = 0;
}
else if(nvtab.nval >= nvtab.max) // grow
{
nvp = (Nameval *) realloc(nvtab.nameval, (NVGROW * nvtab.max) * sizeof(Nameval));
if(nvp == NULL)
{
return -1;
}
nvtab.max *= NVGROW;
nvtab.nameval = nvp;
}
nvtab.nameval[nvtab.nval] = newname;
return nvtab.nval++;
}
函数addname返回刚加入数组的小标,出错返回-1.
对realloc调用将把数组增长到一个新的规模,并保持已有的元素不变。该函数返回指向新数组的指针;当存储不够时返回NULL。由于重新分配,数组的位置可能改变,程序其他部分对数组元素的访问必须通过下标进行,而不能通过指针。
上面代码如果采用下面的方式:
如果采用这种方式,当重新分配失败时原来的数组就会丢失。
按说realloc的返回值不必强制转换到最后的类型,因为C能自动完成对void*的提升。而C++中就不同,必须做类型强制转换。
强制转化 ==> 清晰而认真
不做强制转化 ==> 强制转化可能掩盖真正的错误。
我们选择强制转化,因为这种写法能同时使用于C和C++,付出的代价就是减少了编译器检出错误的可能性。
删除name需要点技巧,因为要决定怎样填补删除后数组中留下的空隙。如果数组顺序并不重要,最简单的方法就是把数组最后的元素复制到这。如果还要保持原有顺序,只能把空洞后所有的元素向前移一个位置:
/* delname: remove the first matching nameval from nvtab */
int delname(char * name)
{
int i;
for (i = 0; i < nvtab.nval; i++)
{
if (strcmp(nvtab.nameval[i].name, name) == 0)
{
memmove(nvtab.nameval + i, nvtab.nameval + i + 1, (nvtab.nval - (i + 1)) * sizeof(Nameval));
nvtab.nval--;
return 1;
}
}
return 0;
}
memmove是标准库函数,用于复制任意大小的存储区块。
在ANSI C 的标准库中定义了两个相关函数: memcpy 的速度快,但是如果源位置和目标位置重叠,有可能覆盖掉存储区中的某些部分;memmove函数的速度可能慢,但总能保证复制的正确完成。尽量使用memmove以避免很容易犯的复制顺序错误。
也可以采用其他方法,不做数组元素移动。例如把被删除数组元素标记为未用。随后要插入元素时,先检索是否有无用位置。
数组是组织数据的最简单方式。数组使用非常简单,提供对元素的O(1)访问,能很好的使用二分检索和快速排序,空间开销也比较小。如果保证数据不太多,数组就是最佳选择。但事情也有另一面,在数组里维护一组不断变化的数据代价很高。所以,如果元素数量无法预计,或者可能会非常多,选择其他的数据结构可能更合适。
7. 表
除了数组之外,链表是典型程序里使用最多的数据结构。C++和Java里已经由程序库实现了,在C语言中必须自己实现。
一个单链表包含一组项,每个项都包含了有关数据和指向下一个项的指针。表的头就是一个指针,指向第一个元素,表的结束则用空指针表示。
数组和表之间的差别:首先,数组具有固定的大小,而表则具有恰好能容纳其所有内容的大小,在每个项目里都需要一个指针的附加存储开销。第二,通过修改指针,表里的各种情况很容易重新进行安排,与数组里需要做大范围的元素移动相比,修改几个指针的代价要小得多。最后,当有某些项被插入或删除时,其他的项都不必移动。
如果一个数据集合里的项经常变化,特别是如果项的数目无法预计时,表是一种可行的存储方式。数组更适合存储相对静止的数据。
表的一些基本操作: 在表前或最后加入一个新项,检索一个特定项,在制定项之前或之后插入一个新项,可能还有删除一个项等等。
无法在编译时初始化一个非空的链表,表应该完全动态构造起来。
下边的例子包括了节点定义,在链表的头或尾添加新节点,以及查找节点:
#define eprintf(...) fprintf (stderr, __VA_ARGS__)
/* emalloc: malloc and report if error */
void * emalloc(size_t n)
{
void * p;
p = malloc(n);
if(p == NULL)
{
eprintf("malloc of %u bytes failed:", n);
}
return p;
}
typedef struct Node Node;
struct Node
{
int value;
Node * next;
};
Node * newnode(int value)
{
Node * newN; newN = (Node *) emalloc(sizeof(Node));
newN->value = value;
newN->next = NULL;
return newN;
}
/* addfront: add newN to front of listp */
Node * addfront(Node * listp, Node * newN)
{
newN->next = listp;
return newN;
}
/* addend: add newN to end of listp */
Node * addend(Node * listp, Node * newN)
{
if(listp == NULL)
{
return newN;
}
Node * p;
for(p = listp; p->next != NULL; p = p->next) ;
{
p->next = newN;
}
return listp;
}
/* lookup: senquential search for val in listp*/
Node * lookup(Node * listp, int val)
{
for(; listp != NULL; listp = listp->next)
{
if(listp->value == val)
{
return listp;
}
}
return NULL;
}
// testing in main method
int _tmain(int argc, _TCHAR* argv[])
{
Node * listp = NULL;
listp = addfront(listp, newnode(1));
listp = addfront(listp, newnode(2));
listp = addend(listp, newnode(3));
Node * p = listp;
while(p != NULL)
{
cout << p->value << " ";
p = p->next;
}
cout << endl;
Node * nptr = lookup(listp, 1);
cout << nptr->value << endl;
}
注:emalloc 调用malloc,如果分配失败,报告一个错误并结束程序。
如果要打印表里所有元素,可以直接写个函数,遍历整个链表并打印元素;要计算表的长度,遍历整个表时递增一个计数器;如此等等。这里提供另一种解决问题的方式,可以定义一个apply函数,遍历表并对每个元素调用另一个函数。
/* apply: execute fn for each element of listp */
void apply(Node * listp, void (* fn)(Node *, void *), void * arg)
{
for(; listp != NULL; listp = listp->next)
{
(*fn)(listp,arg);
}
}
/* printnv: print value using format in arg */
void printnv(Node * p, void * arg)
{
char * fmt;
fmt = (char *) arg;
printf(fmt, p->value);
}
/* inccounter: increment counter *arg */
void inncounter(Node * p, void * arg)
{
int * ip;
ip = (int *) arg;
(*ip)++;
}
int _tmain(int argc, _TCHAR* argv[])
{
int i;
Node * listp = NULL;
listp = addfront(listp, newnode(1));
listp = addfront(listp, newnode(2));
listp = addend(listp, newnode(3));
apply(listp, printnv, "%x\n");
int n = 0;
apply(listp, inncounter, &n);
printf("the length of listp is %x\n", n);
}
在销毁一个链表时要特别小心:
/* freeall: free all elements of listp */
void freeall(Node * & listp)
{
Node * p = listp;
Node * next;
for(; p != NULL; p = next)
{
next = p->next;
free(p);
}
listp = NULL;
}
注:1). 通过指针引用的方式传递listp,目的是置listp 为NULL,防止指向链表头的指针成为野指针。当然也可以在不在freeall里置NULL,而在调用之后再置空。
2). 当一块存储区域被释放之后,程序里就不能再使用它了。因此,在释放listp指向元素之前,必须把listp->next 保存在一个局部变量里。
表特别适合那些需要在中间插入和删除的情况,也适用于管理一批规模经常变动的无顺序数据。如果同时还必须应付频繁的更新和随机访问,那么最好就是使用某些非线性的数据结构,例如树或散列表等。
链表其他常见操作如下(在特定项前或后插入新项,删除特定项,反转链表)
/* addnewbefore: add the new node before the element val if there is one, for the first matching */
Node * addnewbefore(Node * listp, int val, Node * newp)
{
Node * pre = NULL;
Node * p = listp;
while(p != NULL)
{
if(p->value == val)
{
if(pre == NULL)
{
newp->next = p;
return newp;
}
else
{
pre->next = newp;
newp->next = p;
break;
}
}
pre = p;
p = p->next;
}
return listp;
}
/* addnewafter: add the new node after the element val if there is one, for the first matching*/
Node * addnewafter(Node * listp, int val, Node * newp)
{
Node * head = listp;
while(listp != NULL)
{
if(listp->value == val)
{
newp->next = listp->next;
listp->next = newp;
break;
}
listp = listp->next;
}
return head;
}
/* deletenode: delete the node val, for the first matching*/
Node * deletenode(Node * listp, int val)
{
Node * pre = NULL;
Node * head = listp;
while(listp != NULL)
{
if(listp->value == val)
{
if(pre == NULL)
{
head = listp->next;
free(listp);
}
else
{
pre->next = listp->next;
free(listp);
}
break;
}
pre = listp;
listp = listp->next;
}
return head;
}
/* reverselist: reverse the list by recursion.*/
Node * reverselist(Node * listp)
{
if(listp == NULL || listp->next == NULL)
{
return listp;
}
else
{
Node * temp = reverselist(listp->next);
listp->next = NULL;
return addend(temp, listp);
}
}
/*reverselist: reverse the list by loop */
Node * reverselistl(Node * listp)
{
if(listp == NULL || listp->next == NULL)
{
return listp;
}
Node * pre = listp;
Node * cur = listp->next;
Node * next = NULL;
while(cur != NULL)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
listp->next = NULL;
return pre;
}
int _tmain(int argc, _TCHAR* argv[])
{
int i;
Node * listp = NULL;
listp = addend(listp, newnode(1));
listp = addend(listp, newnode(2));
listp = addend(listp, newnode(3));
listp = addend(listp, newnode(4));
listp = addend(listp, newnode(5));
listp = addend(listp, newnode(6));
listp = addend(listp, newnode(7));
listp = addend(listp, newnode(8));
cout << "add new node before" << endl;
listp = addnewbefore(listp, 1, newnode(100));
apply(listp, printnv, "%d\n");
cout << "add new node after" << endl;
listp = addnewafter(listp, 2, newnode(200));
apply(listp, printnv, "%d\n");
cout << "delete node 7" << endl;
listp = deletenode(listp, 7);
apply(listp, printnv, "%d\n");
cout << "reverse the list by recursion" << endl;
listp = reverselist(listp);
apply(listp, printnv, "%d\n");
cout << "reverse the list by loop" << endl;
listp = reverselistl(listp);
apply(listp, printnv, "%d\n");
}
8. 树
树是一种分层性数据结构。在一棵树立存储着一组项,每个项存储一个值,它可以有指针指向0个或多个元素,但是只能被另一个项所指。树根是其中唯一的例外,没有其他项的指针指向它。
用二分检索树说明树的原理,结点中存储的值定义了树结构:对于一个特定结点,其左子树中存储着较小的值,而右子树里存储着较大的值。
/* definition node for tree*/
typedef struct TreeNode TreeNode;
struct TreeNode
{
int value;
TreeNode * left; // lesser
TreeNode * right; // greater
};
由于树的很多结点包含多个指向其他元素的指针,所以,很多在表或者数组结构中需要O(n)时间的操作,在树中只需要O(logn)时间。
二分检索树的构造方式:在树里递归地向下,根据情况确定向左或向右,直接找到了链接新结点的正确位置。结点应该正确地初始化为TreeNode对象,它包括一直值和两个空指针。新结点总是作为叶子加进去,它没有子结点。
/* newtreenode: create a new tree node*/
TreeNode * newtreenode(int value)
{
TreeNode * newp;
newp = (TreeNode *) emalloc(sizeof(TreeNode));
newp->value = value;
newp->left = NULL;
newp->right = NULL;
return newp;
}
/* insert: insert newp in treep, return treep */
TreeNode * insert(TreeNode * treep, TreeNode * newp)
{
int cmp;
if ( treep == NULL)
{
return newp;
}
if (treep->value == newp->value)
{
printf("insert: duplicate entry %s ignored",newp->value);
}
else if (newp->value < treep->value)
{
treep->left = insert(treep->left, newp);
}
else
{
treep->right = insert(treep->right, newp);
}
return treep;
}
平衡二叉树严格定义:
一棵空树是平衡二叉树;
若 T 是一棵非空二叉树,其左、右子树为 TL 和 TR ,令 hl 和 hr 分别为左、右子树的深度。当且仅当 ①TL 、 TR 都是平衡二叉树;② | hl - hr |≤ 1;时,则 T 是平衡二叉树。
二叉树检索,和二分检索思想相同,都是“分而治之”,这是产生对数时间性能的根源。
/* lookup: look up node(val) in tree treep */
TreeNode * lookup(TreeNode * treep, int value)
{
if (treep == NULL)
{
return NULL;
}
if (treep->value == value)
{
return treep;
}
else if (treep->value > value)
{
return lookup(treep->left, value);
}
else
{
return lookup(treep->right, value);
}
}