hao对《数据结构与算法分析》的笔记(11)
-
第30页 算法分析
数学基础: 定义:如果存在正常数c和n0使得当N >= n0时 T(N) <= c*f(N),则记为 T(N) = O(f(N)) 定义:如果存在正常数c和n0使得当N >= n0时 T(N) >= c*g(N),则记为 T(N) = Ω(g(N)) 定义:T(N) =
\begin{equation} \Theta(h(N)) \end{equation}当前仅当T(N) = O(h(N))且T(N) = Ω(h(N)). 定义:如果T(N) = O(p(N)) 且T(N)
\begin{equation} \neq \end{equation}\begin{equation} \Theta(p(N)) \end{equation},则T(N) = o(p(N)). 法则1: 如果T1(N)=O(f(N)) 且T2(N) = O(g(N)),那么 a. T1(N) + T2(N) = max(O(f(N)), O(g(N))) b. T1(N) * T2(N) = O(f(N) * g(N)) 法则2: 如果T(N)是一个k次多项式,则T(N) =
\begin{equation} \Theta(N^{k}) \end{equation}法则3: 对于任意常数k,\log ^{k}N = O(N).它告诉我们对数增长非常缓慢。 一般法则: 法则1--------for循环 一次for循环的运行时间至多应该是for循环内语句(包括测试)的运行时间乘以迭代的次数。 法则2--------嵌套的for循环 从里向外分析这些循环。在一组循环嵌套循环内部的一条语句总的运行时间为该语句的运行时间乘以该组所有for循环的大小乘积。 法则3-----顺序语句 将各个语句的运行时间求和即可(这意味着,其中最大值就是所得的运行时间) 法则4-------if/esle语句 一个if/else语句的运行时间从来不超过判断加上if语句块和else语句块中运行时间长者的总的运行时间。 最大子序列求和算法:
int MaxSubsequenceSum(const int A[], int N) { int ThisSum = 0, MaxSum = 0, j; for( j = 0; j < N; j++) { ThisSum += A[j] if(ThisSum > MaxSum) MaxSum = ThisSum; else if(ThisSum < 0) ThisSum = 0; } return MaxSum }
上面的代码无非不就是最大值状态的更新转移。消除多重循环。 对分查找:
int BinarySearch(const ElementType A[], ElementType X, int N) { int Low = 0, Mid, High = N - 1; while(Low <= High) { Mid = (Low + High) / 2; if(A[Mid] < X) Low = Mid + 1; else if(A[Mid] > X) High = Mid - 1; return Mid } return NotFound; }
欧几里得算法: 计算两个数的最大公因数
unsigned int GCD (unsigned int M, unsigned int N) { unsigned int Rem; while( N > 0) { Rem = M % N; M = N; N = Rem; } return M; }
-
第64页 表,栈和队列
数据抽象类型(ADT)是一些操作的集合。抽象数据类型是数学的抽象;在ADT的定义中根本没有涉及如何实现操作的集合。这可以看成模块化设计的扩充。 引自 表,栈和队列 表:
对表的操作可以用数组来实现。但是需要对表的大小的最大值进行估计,通常需要估计得大一些,会浪费大量的空间。这是严重的局限,特别是存在许多未知大小的表的情况下。所以简单数组一般不用来实现表这种结构。 引自 表,栈和队列 链表在内存中不必相连。 链表的类型声明:
#ifndef _List_H struct Node; typedef struct Node *PtrToNode; typedef PtrToNode List; typedef PtrToNode Position; List MakeEmpty(List L); int isEmpty(List L); int isLast(Position P, List L); Position Find(ElementType X, List L); void Delete(ElementType X, List L); Position FindPrevious(ElementType X, List L); void Insert(ElementType X, List L, Position P); void DeleteList(List L); Position Header(List L); Position First(List L); Position Advance(Position P); ElementType Retrieve(Position P); #endif //Place in the implement file struct Node{ ElementType Element; Position Next; };
测试一个链表是否是空链表:
//Return True if L is empty int isEmpty(List L) { return L->Next == NULL; }
测试当前位置是否是链表的末尾:
int isLast(Position P, List L) { return P->Next == NULL; }
查找:
Position Find(ElementType X, List L) { Position P; P = L->Next; while(P != NULL && P -> Element != X) { P = P -> Next; } return P; }
删除链表节点:
void Delete(ElementType X, List L) { Position P, TmpCell; P = FindPrevious(X, L) if(!IsLast(P, L)) { TmpCell = P -> Next; P -> Next = TmpCell -> Next; free(TmpCell); } }
查找前驱结点:
Position FindPrevious(ElementType X, List L) { Position P; P = L; while(P->Next != NULL && P -> Next -> Element != X) { P = P -> Next; } return P; }
链表的插入:
void Insert(ElementType X, List L, Position P) { Position TmpCell; TmpCell = malloc(sizeof(struct Node)); if(TmpCell == NULL) Error("Out of Place"); TmpCell -> Element = X; TmpCell -> Next = P -> Next; P -> Next = TmpCell; }
删除链表:
void DeleteList(List L) { Position P,Tmp; P = L -> Next; L -> Next = NULL; while(P != NULL) { Tmp = P -> Next; free(P); P = Tmp; } }
栈的ADT:
任何表的形式都能实现栈。 引自 表,栈和队列 栈的链表实现: 栈ADT链表实现的类型声明:
#ifndef _Stack_H struct Node; typedef struct Node *PtrToNode; typedef PtrToNode Stack; int IsEmpty(Stack S); Stack CreateStack(void); void DisposeStack(Stack S); void MakeEmpty(Stack S); void Push(ElementType X, Stack S); ElementType Top(Stack S); void Pop(Stack S); #endif struct Node{ ElementType Element; PtrToNode Next; };
测试栈是否为空栈:
int IsEmpty(Stack S) { return S -> Next == NULL; }
创建空栈:
Stack CreateStack(void) { Stack S; S = malloc(sizeof(struct Node)); if(S == NULL) Error("Out of space"); S -> Next = NULL; MakeEmpty(S); reurn S; } void MakeEmpty(Stack S) { if(S == NULL) Error("Must use CreateStack First!"); else while(!IsEmpty(S)) Pop(S); }
Push进栈:
void Push(ElementType X, Stack S) { PtrToNode TmpCell; TmpCell = malloc(sizeof(struct Node)); if(TmpCell == NULL) Error("Out of space"); else { TmpCell -> Element = X; TmpCell -> Next = S -> Next; S -> Next = TmpCell; } }
返回栈顶元素:
ElementType Top(Stack S) { if(!IsEmpty(S)) return S -> Next -> Elemnt; Error("Empty Stack"); return 0; }
从栈弹出元素:
void Pop(Stack S) { PtrToNode FirstCell; if(IsEmpty(S)) Error("empoty stack"); else { FirstCell = S -> Next; S -> Next = S -> Next -> Next; free(FirstCell); } }
队列模型:
队列的基本操作时入队,它是在表的末端插入一个元素,还有出队,它是删除在表开头的元素。 队列一般被用于处理用概率方法计算用户排队预计等待时间,等待服务的队列。诸如此类的问题被称为排队论。 引自 表,栈和队列 队列的ADT类型声明:
#ifndef _Queue_H struct QueueRecord; typedef struct QueueRecord *Queue; int IsEmpty(Queue Q); int IsFull(Queue Q); Queue CreateQueue(int MaxElements); void DisposeQueue(Queue Q); void MakeEmpty(Queue Q); void Enqueue(ElementType X, Queue Q); ElementTpe Front(Queue Q); void Dequeue(Queue Q); ElementType FrontAndDequeue(Queue Q); #endif #define MinQueueSize 5 struct QueueRecord{ int Capacity; int Front; int Rear; int Size; ElementType *Array; };
测试队列是否为空(数组实现):
int IsEmpty(Queue Q) { return Queue -> Size == 0; }
构造空队列:
void MakeEmpty(Queue Q) { Q -> Size = 0; Q -> Front = 1; Q -> Rear = 0; }
入队:
static int Succ(int Value, Queue Q) { if(++ Value == Q -> Capacity) Value = 0; return Value; } void Enqueue(ElementType X,Queue Q) { if(IsFull(Q)) Error("Full Queue"); else { Q -> Size ++; Q -> Rear = Succ(Q -> Rear, Q); Q -> Array[Q -> Rear] = X; } }
-
第100页 树
算法分析评估里面的N是代表输入数据的规模 对于大量数据的输入,链表的线性访问时间太慢,不宜使用。树这种数据结构的运行时间平均为O(log N)。树的一种自然的定义方式是递归方法。 引自 树 具有相同父亲的节点为兄弟(sibling).一个树的深度等于它的最深树叶的深度,该深度总是等于这棵树的高度。 树节点的定义:将灭个节点的所有儿子都放在树节点的链表中。 引自 树 树的遍历和应用:
树有很多应用。最流行的用法之一就是UNIX,VAX/VMS和DOS在内常用操作系统的目录结构。严格来说UNIX文件系统不是树,是类树(treelike)。 引自 树 二叉树: 二叉树(binary tree)是一棵树,其中每个节点都不能有多于两个的儿子。二叉树的一个性质是平均二叉树的深度要比N小得多,这个性质很重要。分析表明,这个平均深度为O(
\begin{equation}\sqrt{N}\end{equation}),而对于特殊类型的二叉树,即二叉查找树(binary search tree),其平均深度为O(log N)。最坏情况深度达到N - 1。
二叉树有许多与搜索无关的重要应用。主要用途之一就是在编译器设计领域。 引自 树 表达式树: 表达式树(expression tree)的树叶是操作数(operand,比如常量或变量,而其它的节点为操作符(operator))。 二叉查找树:
使二叉树成为二叉查找树的性质是,对于树中的每个节点X,它在左子树中所有关键字值小于X的关键字值,而它右子树中所有关键字值大于X的关键字值。 引自 树 因为二叉查找树的平均深度为O(log N),所以一般递归建树,不用担心栈空间被耗尽。 二叉查找树声明:
#ifndef _TREE_H struct TreeNode; typedef struct TreeNode *Position; typedef struct TreeNode *SearchTree; SearchTree MakeEmpty(SearchTree T); Position Find(ElementType X, SearchTree T); Position FindMin(SearchTree T); Position FindMax(SearchTree T); SearchTree Insert(ElementType X, SearchTree T); SearchTree Delete(ElementType X, SearchTree T); ElementType Retrieve(Position P); #endif struct TreeNode{ ElementType Element; SearchTree Left; SearchTree Right; }
建立一棵空树:
SearchTree MakeEmpty(SearchTree T) { if(T != NULL) { MakeEmpty(T->Left); MakeEmpty(T->Right); free(T); } return NULL; }
Find操作:
Position Find(ElementType X, SearchTree T) { if(T == NULL) return NULL; if(X < T -> Element) return Find(X, T -> Left); else if(X > T->Element) return Find(X, T -> Right); else return T; }
FindMin的递归实现:
Position FindMin(SearchTree T) { if(T == NULL) return NULL; else if(T -> Left == NULL ) return T; else return FindMin(T -> Left); }
FinMax的非递归实现:
Position FindMax(SearchTree T) { if(T != NULL) while(T -> Right != NULL) { T = T -> Right; } return T; }
插入元素到二叉查找树:
SearchTree Insert(ElementType X, SearchTree T) { if(T == NULL) { T = malloc(sizeof(struct TreeNode)); if(T == NUU) Error("Out of space"); else { T -> Element = X; T -> Left = T -> Right = NULL; } } else if(X < T -> Element) { T -> Left = Insert(X, T); } else if(X > T -> Element) { T -> Right = Insert(X, T); } return T; }
二叉查找树的删除:
正如许多数据结构那样,最困难得是删除。如果删除次数不多,则通常使用的策略是懒惰删除(lazy Deletion)。当一个元素要被删除时,它仍然留在树中,而是只是做了个被删除的记号。 引自 树 SearchTree Delete(ElementType X, SearchTree) { Position TmpCell; if(T == NULL) Error("Element not Found"); else if(X < T -> Element) T -> Left = Delete(X, T -> Left); else if(X > T -> Element) T -> Right = Delete(X, T -> Right); else if(T -> Left && T -> Right) //Two Children { TmpCell = FindMin(T -> Right); T -> Element = TmpCell -> Element; T -> Right = Delete(T -> Element, T -> Right); } else //One or Zero Children { TmpCell = T; if(T -> Left == NULL) T = T -> Right; else if(T-> Right == NULL) T = T -> Left; free(TmpCell); } return T; }
一棵树的所有节点的深度的和称为内部路径长。 引自 树 AVL树: AVL树是带有平衡条件的二叉查找树。 B树: 一种常用的查找树(不是二叉树),叫B树。
阶为M的B树是一棵具有下列结构特性的树: a.树的根或者是一片树叶,或者其儿子数在2和M之间。 b.根除外,所有非树叶节点的儿子树在[M/2](向上取整)和M之间。 c.所有的树叶都在相同的深度上。 所有的数据都在树叶上。 B树实际用于数据库系统中,在那里的树被存储在物理磁盘上,而不是内存中 引自 树 总结: 分析树是编译器设计的核心数据结构。分析树不是二叉树,而是表达式树相对简单的扩充。查找树的非递归实现多少要快一些。查找树的问题在于,其性能严重的依赖输入,而输入时随机的。如果情况不是这样,则运行时间会显著增加,查找树会成为昂贵的链表。
-
第131页 散列
散列表的实现常常叫做散列(hashing)。散列是一种用于以常数平均时间执行插入,删除和查找的技术。但是,需要元素间任何排序信息的操作将不会得到有效支持。 引自 散列 散列表的通常关键字不是整数,是字符串。 一种方法是把字符串的ASCII码加起来。(简单的散列函数)
typedef usigned int Index; Index Hash(const char* key, int TableSize) { unsigned int HashVal = 0; while(*key != '\0') HashVal += *key++; return HashVal % TableSize; } //不太好的hash函数 Index Hash(const char *key, int TableSize) { return (key[0] + 27*key[1] + 729*key[2]) % TableSize; } //一个好的散列函数 Index Hash(const char *key, int TableSize) { unsigned int HashVal = 0; while(*key != '\0') HashVal = (HashVal << 5) + *key++; return HashVal % TableSize; }
如果当一个元素被插入时另一个元素已经存在(散列值相同),那么就产生一个冲突,这个冲突需要消除。一般有两种:分离连接法和开放定址法。 分离链接法的做法就是将散列到同一个值的所有元素保留到一个表中。
分离链接散列表的类型声明:
#ifndef _HASHSEP_H struct ListNode; typedef struct ListNode *Position; struct HashTbl; typedef struct HashTbl *HashTable; HashTable InitializeTable(int TableSize); void DestoryTable(HashTable H); Position Find(ElementType Key, HashTable H); void Insert(ElementType Key, HashTable H); ElementType Retrieve(Position P); #endif struct ListNode{ ElementType Element; Position Next; }; typedef Position List; struct HashTbl{ int TableSize; List *TheLists; }
分离链接散列表的初始化:
HashTable IinitializeTable(int TableSize) { HashTable H; int i; if(TableSize < MinTableSize) { Error("TableSize too small"); } H = malloc(sizeof(struct HashTbl)); if(H == NULL) Error("Out of space"); H -> TableSize = NextPrime(TableSize); H -> TheLists = malloc(sizeof(List) * H -> TableSize); if(H -> TheLists == NULL) Error("Out of space"); for(i = 0; i < H -> TableSize; i++) { H -> TheList[i] = malloc(sizeof(struct ListNode)); if(H -> TheLists[i] == NULL) Error("Out of Space"); else H -> TheLists[i] -> Next = NULL; } return H; }
分离链接散列表的Find:
Position Find(ElementType Key, HashTable H) { Position P; List L; L = H ->TheLists[Hash(Key, H -> TableSize)]; P = L ->Next; while(P != NULL && P -> Element != Key) { P = P -> Next; } return P; }
分离链接散列表的Insert:
void Insert(ElementType Key, HashTable H) { Position Pos, NewCell; List L; Pos = Find(Key, H); if(Pos == NULL) { NewCell = malloc(sizeof(struct ListNode)); if(NewCell == NULL) Error("Out of space"); else { L = H -> TheLists[Hash(Key, H->TableSize)]; NewCell -> Next = L -> Next; NewCell -> Element = Key; L -> Next = NewCell; } } }
开放定址法:
分离链表法的缺点是需要指针,由于给新单元分配地址需要时间,因此就导致算法的速度多少减慢。在开放定址法中,如果有冲突发生,就尝试选择另外的单元,知道找到空的单元为止。 引自 散列 开放定址法有三种冲突解决方法:线性探测,平方探测,双散列。 平方探测是消除线性探测探测中一次聚集的问题。平方探测就是冲突函数为二次函数的探测方法。对于线性探测,让元素几乎填满散列表并不是个好主意,因为此时表的性能会下降。 定理:如果使用平方探测法,且表的大小是素数,那么当表至少有一半是空的时候,总能插入一个新元素。 开放定址散列表的类型声明:
#ifndef _HASHQUAD_H typedef unsigned int Index; typedef Index Position; struct HashTbl; typedef HashTbl *HashTable; HashTable InitializeTable(int TableSize); void DestoryTable(HashTable H); Position Find(ElementType Key, HashTable H); void Insert(ElementType Key, HashTable H); ElemenType Retrieve(Position P, HashTable H); HashTable Rehash(HashTable H); #endif enum KindOfEntry{ Legitimate, Empty, Deleted }; struct HashEntry{ ElementType Element; enum KindOfEntry Info; }; typedef struct HashEntry Cell; struct HashTbl{ int TableSize; Cell *TheCells; };
初始化开放定址表:
HashTable InitializeTable(int TableSize) { HashTable H; int i; if(TableSize < MinTableSize) { Error("TableSize too small"); } H = malloc(sizeof(struct HashTbl)); if(H == NULL) Error("Out of space"); H -> TableSize = NextPrime(TableSize); H -> TheCells = malloc(sizeof(Cell) * H -> TableSize); if(H -> TheCells == NULL) Error("Out of space"); for(i = 0; i < H -> TableSize; i++) { H -> TheCells[i].Info = Empty; } return H; }
使用平方探测散列表的Find
Position Find(ElementType Key, HashTable H) { Position CurrentPos; int CollisionNum; CollisionNum = 0; CurrentPos = Hash(Key, H -> TableSize); while(H -> TheCells[CurrentPos].Info != Empty && H ->TheCells[CurrentPos].Element !=Key ) { CurrentPos += 2* ++CollisionNum -1; if(CurrentPos >= H -> TableSize) CurrentPos -= H -> TableSize; } return CurrentPos; }
使用平方探测散列表的插入:
void Insert(ElementType Key, HashTable H) { Position Pos; Pos = Find(Key, H); if(H -> TheCells[Pos].Info != Legitmate) { H -> TheCells[Pos].Info = Legitmate; H -> TheCells[Pos].Element = Key; } }
再散列:
HashTable Rehash(HashTable H) { int i, OldSize; Cell *OldCells; OldCells = H -> TheCells; OldSize = H -> TableSize; H = InitializeTable(2*OldSize); for(i = 0; i < OldSize; i++) { if(OldCells[i].Info == Legitmate) Insert(OldCells[i].Element, H); } free(OldCells); return H; }
总结: 散列表有丰富的应用。编译器使用散列表跟踪源代码中声明的变量,符号表。游戏中的变换表。拼写纠错。
-
第164页 优先队列(堆)
1。模型
优先队列至少允许两种操作的数据结构:Insert(插入),它的工作是显而易见的,以及DeleteMin(删除最小者),它的工作是找出,返回和删除优先队列中最小的元素。 引自 优先队列(堆) 2.一些简单实现
用链表,以O(1)在表头执行插入,再以O(N)遍历找出最小元。用二叉查找树,如果用二叉查找树,DeleteMin会反复删除最小元,右子树加重,损害树的平衡 引自 优先队列(堆) 3.二叉堆
堆有两个性质:结构性和堆序性。对堆的一次操作可能破坏两个性质中的一个,因此,堆操作必须要到堆的所有性质都被满足时才能终止。 引自 优先队列(堆) 3.1结构性质
堆是一棵被完全填满的二叉树,有可能的例外是在底层,底层上的元素从左到右填入。这样的树称为完全二叉树。容易证明,一棵高为h的完全二叉树,节点有2^h到2^(h+1) - 1个节点。所以完全二叉树的高是floor(log N),显然它是O(log N)。 完全二叉树很有规律,它可以用一个数组来表示,不需要指针。 对于数组中任意一位置i上的元素,其左儿子在位置2i上,右儿子在左儿子后的(2i + 1)中,它父亲则在位置floor(i / 2)上。一个堆结构将由一个数组,一个代表最大值的整数以及当前的堆大小组成。 引自 优先队列(堆) 优先队列的声明
#ifndef _BINHEAP_H struct HeapStruct; typedef struct HeapStruct *PriorityQueue; PriorityQueue Initialize(int MaxElements); void Destroy(PriorityQueue H); void MakeEmpty(PriorityQueue H); void Insert(ElementType X, PriorityQueue H); ElementType DeleteMin(PriorityQueue H); ElementType FindMin(PriorityQueue H); int IsEmpty(PriorityQueue H); int IsFull(PriorityQueue H); #endif struct HeapStruct{ int Capacity; int Size; ElementType *Elements; };
3.2堆序性质
使操作被快速执行的性质被称为堆序性。在一个堆中,对于每个节点X,X的父亲中的关键字小于(或等于)X中的关键字,根节点除外(它没有父亲)。根据堆序性,最小元总可以在根处找到。 引自 优先队列(堆) 初始化优先队列:
PriorityQueue Initialize(int MaxElements) { PriorityQueue H; if(MaxElements < MinPQSize) Error("PQ is too small!"); H = malloc(sizeof(struct HeapStruct)); if(H == NULL) Error("Out of space!"); H -> Elements = malloc((MaxElements + 1) * sizeof(ElementType)); if(H -> Elements == NULL) Error("Out of space"); H -> Capacity = MaxElemets; H -> Size = 0; H -> Elements[0] = MinData; return H; }
3.3基本的堆操作
Insert的一般策略是上滤(percolate up).新元素在堆中上滤直到找出正确的位置。 把一个小于或等于堆中任何元素的值放到位置0,使得循环得以终止。我们称为标记。 引自 优先队列(堆) 插入过程:
void Insert(ElementType X, PriorityQueue H) { int i; if(IsFull(H)) { Error("PQ is Full"); return; } for(i = ++ H-> Size; H -> Elements[i / 2] > X; i /= 2) H -> Elements[i] = H -> Elements[i / 2]; H -> Elements[i] = X; }
删除最小元:
一般策略为下滤。 引自 优先队列(堆) ElementType DeleteMin(PriorityQueue H) { int i, Child; ElementType MinElement, LastElement; if(IsEmpty(H)) { Error("PQ is empty!"); return H -> Elements[0]; } MinElement = H -> Elements[1]; LastElement = H -> Elements[H -> Size --]; for(i = 1; i * 2 <= H -> Size; i = Child) { Child = i * 2; if(Child != H -> Size && H -> Elements[Child + 1] < Elements[Child]) Child++; if(LastElement > H -> Elements[Child]) H -> Elements[i] = H -> Elements[Child]; else break; H -> Elements[i] = LastElement; return MinElement; } }
3.4其他的堆操作
包含2^(h + 1) - 1 个节点高为h的理想二叉树的节点的高度和为2^(h + 1) - 1 - (h + 1)。 引自 优先队列(堆) 3.5优先队列的应用 1.选择问题 2.事件模拟
-
第198页 排序
1.插入排序
插入排序由N - 1趟排序组成。对于P = 1趟到 P = N - 1趟,插入排序保证从位置0到位置P上的元素为已排序状态。它利用了一个事实:位置0到位置P - 1上的元素是已排过序的。 引自 排序 void InsertionSort(ElementType A[], int N) { int j, P; ElementType Tmp; for(P = 1; P < N; P++) { Tmp = A[P]; for(j = P; j > 0 && A[j - 1] > Tmp ; j--) A[j] = A[j - 1]; A[j] = Tmp; } }
插入排序分析
由于嵌套循环的每一个都花费N次迭代,因此插入排序为O(N^2)。 引自 排序 一些简单排序算法的下界
若逆序数是O(N),则插入排序以线性时间运行。 N个互异数的数组的平均逆序数是N( N - 1)/4。 通过交换相邻元素进行排序的任何算法平均需要Ω(N^2)时间。 引自 排序 2.希尔排序 希尔排序有时候也叫做缩小增量排序 希尔排序使用一个序列h1,h2,h3,....ht,叫做增量序列。 希尔排序的一个重要性质是,一个hk-排序的文件(此后将是h(k-1)-排序的)保持它的hk 排序性。 增量序列的一种流行(但是不好)的选择是使用shell建议的序列:ht = floor[N / 2]和hk = floor[h(k+1) / 2]. TIPS:t 和k和k-1,k+1都是下标.
void ShellSort(ElementType A[], int N) { int i, j, Increment; ElementType Tmp; for(Increment = N / 2; Increment > 0; Inrement /= 2) for(i = Increment; i < N; i++) { Tmp = A[i]; for(j = i; j >= Increment; j-= Increment) if(Tmp < A[j - Increment]) A[j] = A[j - Increment]; else break; A[j] = Tmp; } }
希尔排序的最坏情形分析
希尔排序的运行时间依赖于增量序列的选择,而证明相当复杂。 引自 排序 使用希尔增量时希尔排序的最坏情形运行时间为Theta(N^2)。 使用Hibbard增量的希尔排序最坏情形运行时间为theta(N^(3 / 2))。 希尔排序的性能在实践中是可以完全接受的,即使是对于数以万计的N仍然如此。编程的简单特点使得它成为对适度大量的输入数据经常选用的算法。 堆排序 优先队列可以用于花费O(N log N)时间的排序。基于该想法的算法叫做堆排序 ,并给出我们至今所见到的最佳大O运行时间。然而,在实践中它却慢于使用Sedgewick增量序列的希尔排序。 该算法的主要问题是它使用了一个附加数组。因此存储需求增加一倍。将第二个数组拷贝回第一个数组的额外时间消耗只是O(N),这不可能显著影响运行时间。避免使用第二个数组的聪明做法是利用这样一个事实:在每次DeleteMin之后,堆缩小了1.因此位于堆中最后的单元可以用来存放刚刚删去的元素。
#define LeftChild(i) (2*(i) + 1) void PercDown(ElementType A[], int i, int N) { int Child; ElemenType Tmp; for(Tmp = A[i]; LeftChild(i) < N; i = Child) { Child = LeftChild(i); if(Child != N -1 && A[Child + 1] > A[Child]) Child ++; if(Tmp < A[Child]) A[i] = A[Child]; else break; } A[i] = Tmp; } void HeapSort(ElemenType A[], int N) { int i; for(i = N / 2; i >= 0; i--) PercDown(A, i, N); for(i = N - 1; i > 0; i--) { Swap(&A[0], &A[i]); PercDown(A, 0, i); } }
堆排序的分析 堆排序是一个非常稳定的算法:它平均使用的比较只比最坏情形情况界指出的略少。然而直到最近,还没有人能够指出堆排序平均运行时间的非平凡界。 对于N个互异项的随机排列进行堆排序,所用的比较平均次数为2N log N - O(N log logN). 堆排序总是使用至少N log N - O(N)次比较,而且存在输入数据能够达到这个界。 归并排序 归并排序以O(N log N)最坏情形时间运行,而所使用的比较次数几乎是最优的。 这个算法中基本的操作是合并两个已排序的表。 基本的合并算法是取两个输入数组A和B,一个输入数组C,以及三个计数器Aptr,Bptr,Cptr,它们初始置于对应数组的开端。A[Aptr]和B[Bptr]中的较小者被拷贝到C中的下一个位置,相关的计数器向前推进一步。当两个输入表有一个用完的时候,则将另一个表中剩余的部分拷贝到C中。 合并两个已排序的表的时间显然是线性的,最多进行了N - 1次比较,其中N是元素的总数。
//合并序列 void Merge(ElemenType A[], ElemenType TmpArray[], int Lpos, int Rpos, int RightEnd) { int i, LeftEnd, NumElements, TmpPos; LeftEnd = Rpos - 1; TmpPos = Lpos; NumElements = RightEnd - Lpos + 1; while(Lpos <= Left && Rpos <= RightEnd) { if(A[Lpos] <= LeftEnd && Rpos <= RightEnd) TmpArray[Tmp ++] = A[Lpos ++]; else TmpArray[TmpPos++] = A[Rpos ++]; } while(Lpos <= LeftEnd) TmpArray[TmpPos++] = A[Lpos ++]; while(Rpos <= RightEnd) TmpArray[TmpPos++] = A[Rpos ++]; for(i = 0; i < NumElemenTypes; i++,RightEnd --) A[RightEnd] = TmpArray[RightEnd]; } //分路 void MSort(ElemenType A[], ElemenType TmpArray[], int Left, int Right) { int Center; if(Left < Right) { Center = (Left + Right) / 2; MSort(A, TmpArray, Left, Center); MSort(A, TmpArray, Center + 1, Right); Merge(A, TmpArray,Left,Center + 1, Right); } } //主排序入口 void MergeSort(ElemenType A[], int N) { ElemenType *TmpArray; TmpArray = malloc(N * sizeof(ElemenType)); if(TmpArray != NULL) { MSort(A, TmpArray, 0, N - 1); free(TmpArrary); } else Error("No Space for TmpArray!"); }
虽然归并排序的运行时间是O(N log N),但是它很难用于主存排序,主要问题在于合并两个排序的表需要线性附加内存,在整个算法中还要花费将数据拷贝到临时数组再拷贝回来这样一些附加工作,其结果严重放慢了排序的速度。 3.快速排序 快速排序是在实践中最快的已知排序算法,它的平均时间是O(N log N)。该算法之所以特别快,主要是由于非常精炼的高度优化的内部循环。 基本由下列简单四个步骤组成: 1.如果S中元素个数是0或1,则返回 2.取S中任一元素v,称之为枢纽元(pivot) 3.将S - {v}(S中其余的元素)分成两个不相交的集合:S1 = {x ∈ S - {v} | x <= v}和S2 = {x ∈ S - {v} | x >= v} 4.返回{quicksort(S1)后,继续v,继而quicksort(S2)}。 选取枢纽元 一般是三数中值分割(随机一串数组,首位两端的元素相加除以2得到的值作为这个数组的索引取得的元素为枢纽元。) 小数组 对于很小的数组(N <= 20),快速排序不如插入排序好。
#define Cutoff (3) ElementType Median3(ElementType A[], int Left, int Right) { int Center = (Left + Right) / 2; if(A[Left] > A[Center]) Swap(&A[Left], &A[Center]); if(A[Left] > A[Right]) Swap(&A[Left], &A[Right]); if(A[Center] > A[Right]) Swap(&A[Center], &A[Right]); Swap(&A[Center], & A[Right - 1]); //Hide pivot return A[Right - i]; } void Qsort(ElementType A[], int Left, int Right) { int i, j; ElemenType Pivot; if(Left + Cutoff <= Right) { Pivot = Median3(A, Left,Right); i = Left; j = Right - 1; for(;;) { while(A[++i] < Pivot){} while(A[--j] > Pivot){} if( i < j) Swap(&A[i], &A[j]); else break; } Swap(&A[i], &A[Right - 1]); Qsort(A, Left, i -1); Qsort(A, i + 1, Right); } else InsertSort(A + Left, Right - Left + 1); } void QuickSort(ElementType A[], int N) { Qsort(A, 0, N - 1); }
可以修改快速排序以解决选择问题 查找集合S中第k个最小元素的算法几乎与快速排序相同。 与快速排序相比,快速选择只做了一次递归调用而不是两次。快速选择的最坏情况和快速排序的相同,也是O(N^2)。但是使用三数中值选取枢纽元的方法使得最坏情况发生的机会几乎微不足道。
void Qselect(ElementType A[], int k, int Left, int Right) { int i, j; ElemenType Pivot; if(Left + Cutoff <= Right) { Pivot = Median3(A, Left,Right); i = Left; j = Right - 1; for(;;) { while(A[++i] < Pivot){} while(A[--j] > Pivot){} if( i < j) Swap(&A[i], &A[j]); else break; } Swap(&A[i], &A[Right - 1]); if(k <= i) Qselect(A, k, Left, i -1); else if(k > i + 1) Qselect(A, k, i + 1, Right); } else InsertSort(A + Left, Right - Left + 1); }
大型结构的排序 我们常常需要通过关键字对大型结构进行排序。例如:工资名单,每个记录由电话,地址,姓名,电话号码等信息组成。如果真交换两个结构是非常昂贵的操作。在这种情况下,实际的解法是让输入数组包含指向结构的指针。我们通过比较指针指向的关键字,并在必要时交换指针来进行排序。这意味着,所有数据运动基本上就像我们对整数排序那样进行过,我们称之为间接排序。 排序的一般下界 归并排序和堆排序在一个常数因子范围内是最优的。 快速排序在相差一个常数因子的范围内平均是最优的。 只用到比较的任何排序算法在最坏情况下都需要upper(log(N!))次比较并平均需要log(N!)次比较。 令T是深度为d的二叉树,则T最多有2^d个树叶。 具有L片树叶的二叉树深度至少是upper(log L) 只使用元素间比较的任何排序算法在最坏情况下至少需要upper(log (N!))次比较 只使用元素间比较的任何排序算法需要进行Ω(N log N)次比较。 外部排序 因为输入的数据不能全部装进内存,所以要提出外部排序。 任何算法都将需要Ω(N^2)次磁带访问。 一般用多路合并。多相合并。 总结: 对于一般的内部排序应用程序,选用的方法不是插入排序,希尔排序,就是快速排序。如果需要对一些大型文件排序,快速排序是应该被选用的方法。合并时外部排序的中心思想。
-
第214页 不相交集ADT
等价关系
若对于每一对元素(a,b),a,b∈S,aRb或者为true或者为false,则称在集合S上定义关系(relation)R。如果aRb是true,那么我们说a与b有关系。 引自 不相交集ADT 等价关系是满足下列三个性质的关系R:
1.(自反性)对于所有的a∈S, aRa; 2.(对称性) aRb,当且仅当bRa; 3.(传递性)若aRb且bRc 则aRc; 电气连通性是一个等价关系。显然它自反,也传递。a自身肯定相连,a连接到b,b必定连接到a,a连接到b,b连接到c,显然a也连接到c。 引自 不相交集ADT 动态等价性问题 TIPS: 给定一个等价关系"~".
输入数据最初是N个集合的类,每个集合含有几个元素。初始的描述是所有的关系均为false(自反的关系除外)。每个集合都有一个不同的元素,从而S(i) ∩ S(j) = 空集 ;这使得这些集合不相交。 此时有两种运算允许进行。第一种运算是Find,它返回包含给定元素的集合(即等价类)的名字。第二种运算是Union,添加关系。如果我们想要添加关系a ~ b,那么我们首先要看是否a和b已经有关系。通过对a,b执行find并检验它们是否在同一个等价类中来完成。如果不在同一类中,用Union运算把含有a和b的两个等价类合并成一个新的等价类。从集合特性来看,U的结果是建立一个新集合,去掉了原来两个集合而保持所有集合的不相交性。这些算法叫并查集算法。 引自 不相交集ADT 该算法是动态的,因为在算法执行过程中,集合可以通过Union运算而发生改变。 我们不进行任何比较元素相关值的操作,而是只需要知道它们的位置。由类似散列的方法确定。 解决动态等价问题方案有两种:1。保证指令Find能够以常数最坏情形运行时间执行 2.保证指令Union能够以常数最坏情形运行时间执行。二者不能同时做到。 引自 不相交集ADT 基本数据结构
不要求Find返回任何特定的名字,当且仅当两个元素属于相同的集合时,作用在这两个元素上的Find返回相同的名字。 一种想法可以用树来表示每个集合,因为树上的每个元素都有相同的根。这样,该根就可以用来命名所在的集合。不一定是二叉树,为了容易表示,需要唯一的信息就是父指针。集合的名字由根处的节点指出。假设树被非显示的存在一个数组里;数组中每个成员P[i]表示元素i的父亲,如果i是根,那么P[i] = 0; 引自 不相交集ADT 采纳Union(X,Y)后新的根是X的约定(可以自由定义)。 不相交集合的类型声明
#ifndef _DISJSET_H_ typedef int DisjSet[Number + 1]; typedef int SetType; typedef int ElementType; void Initialize(DisjSet S); void SetUnion(DisjSet S, SetType Root1, SetType Root2); SetType Find(ElemenType X, DisjSet S); #endif
void Initialize(DisjSet S) { int i; for(i = NumSets; i > 0; i--) S[i] = 0; } //Union例程(不是最好) void SetUnion(DisjSet S, SetType Root1, SetType Root2) { S[Root2] = Root1; } SetType Find(ElementType X, DisjSet S) { if( S[X] <= 0 ) return X; else return Find(S[X], S); }
灵巧的求并算法 1.对以上的方法简单改进,是借助任意方法打破现有关系,使得总让较小的树成为较大的树的子树,这种方法称为按大小求并。如果这些Union都是按大小求并,那么任何节点的深度均不会超过log N。 2.按高度求并,同样保证所有树的深度最多是O(log N).我们跟踪每棵树的高度而不是大小并执行那些Union使得浅的树成为深的树的子树。。这是一种平缓的算法,因为只有当两颗相等的树求并时树的高度才增加(此时树的高度增1)。
// assume Root1 and Root2 are roots void SetUnion(DisjSet S, SetType Root1, SetType Root2) { if( S[Root2] < S[Root1]) S[Root1] = Root2; else { if(S[Root1] == S[Root2]) S[Root1]--; S[Root2] = Root1; } }
路径压缩
执行Union操作的任何算法都将产生相同的最坏情形的树,因为它必然会随意打破树间的均衡。因此,无须对整个数据结构重新加工而使算法加速的唯一方法是对Find操作做一些更聪明的工作。这方法叫路径压缩。 引自 不相交集ADT 路径压缩在一次Find操作期间执行而与用来执行Union的方法无关。设操作Find(X),此时路径压缩的效果是,从X到根的路径上的每一个节点都使它的父节点变成根 引自 不相交集ADT SetType Find(ElementType X, DisjSet S) { if(S[X] <= 0) return X; else return S[X] = Find(S[X], S); }
Union/Find的算法分析
当执行一系列Union指令时,一个秩(每棵树所存储的高度是估计的高度称为秩rank)为r的节点必然至少有2^r个后裔(包括它自己)。 秩为r的节点的个数最多是N/(2^r)。 在Uion/Find算法的任意时刻,从树叶到根的路径的节点的秩单调增加。 M次Union和Find的运行时间为O(M log*N)。 引自 不相交集ADT -
第258页 图论算法
若干定义
一个图(graph)G = (V,E)由顶点(vertex)集V和边(edge)集E组成。每一条边就是一个点对(v,w),其中v,w∈V。有时也把边称为弧(arc)。如果点对是有序的就是有向图,无序的就是无向图。有时候边还有第三种成分,称为权(weight)或值(cost)。 如果在一个无向图中从每一个顶点到每个其他顶点都存在一条路径,则称该无向图是连通的。具有这样性质的有向图是强连通的。如果一个有向图不是强连通的,但是它是基础图,即其弧上去掉方向所形成的图,是连通的,那么该有向图称为是弱连通的。完全图是其每一对顶点间都存在一条边的图。 引自 图论算法 图的表示
表示图的一种简单方法是使用一个二维数组,称为邻接矩阵表示法。对于每条边(u,v),我们置A[u][v]为1;否则就为0.如果边有一个权,那么我们可以置A[u][v]等于该权,而使用一个很大或者很小的权作为标记表示不存在的边。 若图是稠密的:| E | = Theta(| V | ^ 2),则邻接矩阵是适合表示的方法。如果图是稀疏的,则更好的解决方法是使用邻接表来表示。就是使用一个表来存放所有邻接的顶点。 引自 图论算法 拓扑排序
拓扑排序是对有向无圈图的顶点的一种排序,它使得如果存在一条从v(i)到v(j)的路径,那么排序中v(j)出现在v(i)的后面。 搜索一个图的算法为BFS。该方法,按层处理顶点:距离开始点最近的那些顶点首先被赋值,而最远的那些顶点最后被赋值。 引自 图论算法 Dijistra算法(对于赋权图)
一般解决单源最短路径。算一种贪婪算法。 引自 图论算法 具有负边值的图
这回Dijistra算法行不通了。一个诱人的方案是将一个常数△加到每一条边上,如此去除负边值,再计算新图的最短路径问题,然后把结果用到原来的图上。 引自 图论算法 最小生成树 1.Prim算法 2.krushal算法 NP-完全性介绍
NP代表非确定型多项式时间 计算机不可能解决碰巧发生的每一个问题。这些“不可能”的问题叫不可判定问题。 一个特殊的不可判定行问题是停机问题 在已知属于NP的所有问题中,存在一个子集,叫做NP-完全(NP-complete)问题。它包含了NP中最难的问题。NP-完全问题有一个性质:即NP中的任一问题都能多项式地规约成NP-完全问题。 为了证明某个问题是NP-完全的,必须证明它属于NP,然后将一个适当的NP完全问题变换到该问题。 引自 图论算法 -
第323页 算法设计技巧
贪婪算法
意味着选择的是某个局部的最优。这种“眼下能够拿到的就拿”的策略即是这类算法名字的来源。当算法终止时,我们希望的局部最优就是全局最优。如果这样,算法就是正确的。否则,只是一次最优解。 引自 算法设计技巧 huffman编码
一种简单的策略可以使一般的大型文件节省25%,而使许多大型的数据文件节省多大50%~60%。这种一般的策略就是让代码的长度从字符到字符是变化不等的,同时保证经常出现的字符其代码短。注意,如果所有的字符都以相同的频率出现,那么要节省空间是不可能的。 该算法是一个两趟扫描算法。第一遍搜集频率数据。第二遍进行编码。显然,对于处理大型文件的程序来说这个性质不是我们所希望的。 引自 算法设计技巧 跳跃表
以O(log N)期望时间支持查找和插入的数据结构。 引自 算法设计技巧 素性测试 费马定律:
费马小定理:如果P是素数,且0 < A < P,那么A^(p-1)恒等于1(mod P) 如果P是素数且0 < X < P,那么X^2 恒等于1(mod P)仅有的两个解为X = 1,P - 1. 引自 算法设计技巧 博弈问题 极大极小策略,alpha-beta裁剪。
-
第344页 摊还分析
Insert,DeleteMin,以及Merge对于二项队列的摊还运行时间分别是:O(1),O(log N),O(log N)。 引自 摊还分析 合并两个斜堆的摊还时间为O(log N)。 摊还分析用于在一些操作间分配的负荷。为了进行分析要构造一个虚的位势函数,这个位势函数度量系统的状态。高位势的数据结构是易变的。 引自 摊还分析
hao的其他笔记 · · · · · · ( 全部427条 )
- P2P网络技术原理与C++开发案例
- 5
- 大话移动通信
- 1
- 图解网络硬件
- 1
- 写给大家看的C++书
- 1
- Orange'S
- 1
- 虚拟机
- 1
- 大规模C++程序设计
- 10
- Linux Shell脚本攻略
- 3
- HTTP权威指南
- 3
- C++ API设计
- 8
- C++语言的设计和演化
- 5
- 构建嵌入式LINUX系统
- 3
- 七周七语言
- 15
- 链接器和加载器
- 1
- Windows网络与通信程序设计
- 2
- Windows核心编程(第5版)
- 16
- C++GUI Qt4编程
- 3
- 图解TCP/IP (第5版)
- 12
- Python学习手册
- 5
- 编写可读代码的艺术
- 14
- 你一定爱读的极简欧洲史
- 2
- 短码之美
- 1
- 程序员的自我修养
- 17
- 美国纽约摄影学院摄影教材(上)
- 2
- Win32多线程程序设计
- 8
- 竹林蹊径
- 4
- 鸟哥的Linux私房菜
- 4
- Linux/Unix设计思想
- 6
- 程序设计语言的形式语义
- 1
- Windows驱动开发技术详解
- 9
- 你必须知道的495个C语言问题
- 5
- Windows内核原理与实现
- 44
- PCI Express 体系结构导读
- 3
- 程序员的数学
- 5
- 深入浅出 MFC 第二版
- 2
- 并发的艺术
- 4
- 群和它的图象表示
- 1
- C语言接口与实现
- 1
- 致命元素
- 2
- 深入解析Windows操作系统
- 51
- Linux C编程一站式学习
- 13
- 淘宝技术这十年
- 1
- 高效程序的奥秘
- 1
- 花田半亩
- 1
- 于丹:重温最美古诗词
- 1
- 诛仙8(大结局)
- 1
- 深入理解计算机系统(原书第2版)
- 9
- C和指针
- 9
- 寒江独钓
- 5
- Windows程序设计
- 9
- 80X86汇编语言程序设计教程
- 3
- 琢石成器
- 30
- 数学分析教程
- 1
- 操作系统概念(第六版)
- 12
- 算法竞赛入门经典
- 10
- C++反汇编与逆向分析技术揭秘
- 12