分类算法之贝叶斯(Bayes)分类器
摘要:旁听了清华大学王建勇老师的 数据挖掘:理论与算法 的课,讲的还是挺细的,好记性不如烂笔头,在此记录自己的学习内容,方便以后复习。
一:贝叶斯分类器简介
1)贝叶斯分类器是一种基于统计的分类器,它根据给定样本属于某一个具体类的概率来对其进行分类。
2)贝叶斯分类器的理论基础是贝叶斯理论。
3)贝叶斯分类器的一种简单形式是朴素贝叶斯分类器,跟随机森林、神经网络等分类器都有可比的性能。
4)贝叶斯分类器是一种增量型的分类器。
二:贝叶斯理论
第一次接触贝叶斯还是本科学概率论的时候,那时候也就只知道做题目,没想到现在还能够在工作和学习中用到它,先复习下相关的基础概率公式吧:
1) 乘法定理:设P(B)>0,则有P(AB) = P(A|B)P(B).
2) 全概率公式:设试验E的样本空间为S,A为E的事件,若事件组B1,B2,…,Bn为S的一个划分,且P(Bi)> 0(i=1,2,…,n),则有
P(A) = P(A|B1)P(B1) + P(A|B2)P(B2) + …+ P(A|Bn)P(Bn).
注:在很多事件问题中P(A)不容易算出来,但是可以很容易的找到S的一个划分:B1,B2,…,Bn,并且P(Bi)和P(A|Bi)为已知或者容易算出,那么就可以根据上式求出P(A).
3)贝叶斯公式:设试验E的样本空间为S,A为E的事件,B1,B2…,Bn为S的一个划分,且P(A)>0,P(Bi)>0(i=1,2,…,n),则有
P(Bi|A) = P(ABi)/P(A) = P(A|Bi)P(Bi)/∑P(A|Bi)P(Bi),i=1,2,…n.
举例:
X是一个待分类的数据元组,由n个属性描述;H是一个假设,例如X属于类C。对于分类问题,我们想计算出概率P(H|X):即已知元组X的每个元素对应的属性值,求出X属于C类的概率。
例如:X的属性值为:age=25,income=$5000,H对应的假设是:X会买电脑。
P(H|X):意思是在已知某客户信息age=25,income=$5000的条件下,该客户会买电脑的概率。
P(H):意思是对于任何给定的客户信息,该客户会购买电脑的概率。
P(X|H):意思是已知客户会买电脑,那么该客户的age=25,income=$5000的概率。
P(X):意思是在我们所有的客户信息集合中,客户的age=25,income=$5000的概率。
所以:P(H|X) = P(X|H)P(H)/P(X)
三:朴素贝叶斯分类器
朴素贝叶斯分类器的工作流程如下:
1:设D为样本训练集;每一个样本X是由n个属性值组成的,X=(x1,x2,…xn);对应的属性集为A1,A2,A3…An;
2: 假设有m个类标签:C1,C2,…Cm.对于某待分类元X,朴素分类器会把P(Ci|X)(i=1,2,…m)值最大的那个类标签Ci认为是X的类别,即朴素贝叶斯分类器预测出X属于类Ci,当且仅当P(Ci|X)>P(Cj|X) (1≤j≤m,j≠i).因此我们的目标就是找出P(Ci|X)中的最大值。
P(Ci|X) = P(X|Ci)P(Ci)/P(X)
对于给定的样本集,P(X)是常数,跟某个具体的类标签没有关联,所以要想找出P(Ci|X)的最大值也就是找出P(X|Ci)P(Ci)的最大值:
如果我们不知道P(Ci)的值,我们可以假设P(C1)=P(C2)=…=P(Cm),当然P(Ci)可以通过估计值来代替,P(Ci)=|Ci, D| /|D|
其中|D|为样本总数,|Ci,D|为D中属于类Ci的样本数。
3:如果n的值特别大,也就是说样本元有很多属性,那么对于P(X|Ci)的计算会相当复杂。所以在朴素贝叶斯中进行了一个假设:即对于样本元中的每个属性,它们都互相条件独立。
对于P(xi|Ci)我们可以从训练集中算出来,其中xi代表在某个具体样本中对应属性Ai的值。
P(xi|Ci)的计算分为两种情况:
1):如果属性Ai的值是分类变量(离散变量),那么P(xi|Ci)等于训练样本空间|D|中,属于类Ci并且对应属性Ai的值等于xi的数目除以样本空间中属于类Ci的样本数目。
2):如果Ai的值是连续型的变量,则P(xi|Ci)的计算会根据高斯分布来计算,设其中均值为μ,标准方差为σ:
4:为了预测X所属的类标签,我们根据前面的步骤可以算出每一个类标签Ci对应的P(X|Ci)P(Ci)值,当某一个类标签Ci有:
P(X|Ci)P(Ci)>P(X|Cj)P(Cj) 对于任意j: 1≤j≤m,j≠i
则我们认为X属于类标签Ci.
四:具体例子分析
这里我们还是用 分类算法之决策树 中的样本数据来进行举例:
样本空间D如下表所示:其中 |D|=14.
属性集合为A{age,come,student,credit_rating} 对应的属性个数n=4.
分类属性为:buys_computer,值为{yes,no} 即C1:buys_computer = yes;C2: buys_computer = no; 分类标签个数 m = 2;
有一待分类的数据元X={age<=30,income=medium,student=yes,credit_rating=fail}.
则根据朴素贝叶斯分类器的工作流程我们可以计算出:
P(Ci):
P(buys_computer = “yes”) = 9/14 = 0.643
P(buys_computer = “no”) = 5/14= 0.357
P(xi|Ci):
P(age = “<=30” | buys_computer = “yes”) = 2/9 = 0.222
P(age = “<= 30” | buys_computer = “no”) = 3/5 = 0.6
P(income = “medium” | buys_computer = “yes”) = 4/9 = 0.444
P(income = “medium” | buys_computer = “no”) = 2/5 = 0.4
P(student = “yes” | buys_computer = “yes) = 6/9 = 0.667
P(student = “yes” | buys_computer = “no”) = 1/5 = 0.2
P(credit_rating = “fair” | buys_computer = “yes”) = 6/9 = 0.667
P(credit_rating = “fair” | buys_computer = “no”) = 2/5 = 0.4
P(X|Ci):
P(X|buys_computer = “yes”) = 0.222 x 0.444 x 0.667 x 0.667 = 0.044
P(X|buys_computer = “no”) = 0.6 x 0.4 x 0.2 x 0.4 = 0.019
P(X|Ci)*P(Ci) :
P(X|buys_computer = “yes”) * P(buys_computer = “yes”) = 0.028
P(X|buys_computer = “no”) * P(buys_computer = “no”) = 0.007
因为0.28>0.007所以X属于类:buys_computer = “yes”.
五:朴素贝叶斯存在的问题
1:零概率问题
在上述的例子中假设在样本数据集中income = medium的样本数为0,那么P(income = “medium” | buys_computer = “yes”) 和
P(income = “medium” | buys_computer = “no”) 都将为0,那么在计算P(X|Ci)*P(Ci)时结果也为0,这样就不好决定X是属于哪一个类。
对于这样的问题的一个解决方案叫做:Laplacian correction或者Laplacian estimator,是以一位法国数学家Pierre Laplace名字命名的。
它的具体做法就是给相应的属性的不同值数目都加1:
假设:有1000个训练样本,其中income=low的数目为10,income=medium的数目为0,income=high的数目为990,则为了避免零概率问题,我们给每一种income的数目加1,及最后结果为 income =medium的数目为1,low的数目为11,high的数目为991.这样也就避免了零概率问题。
2:准确度问题
朴素贝叶斯分类器是基于样本属性条件独立的假设的前提下的,但是实际情况可能并不成立,这样也就缺失准确性了.
解决朴素贝叶斯准确性问题提出的一种方法叫做:贝叶斯网络(Bayesian Belief Networks ).这个方法留着下次学习。
各种排序算法总结和比较
排序算法可以说是一项基本功,解决实际问题中经常遇到,针对实际数据的特点选择合适的排序算法可以使程序获得更高的效率,有时候排序的稳定性还是实际问题中必须考虑的,这篇博客对常见的排序算法进行整理,包括:插入排序、选择排序、冒泡排序、快速排序、堆排序、归并排序、希尔排序、二叉树排序、计数排序、桶排序、基数排序。
代码都经过了CodeBlocks的调试,但是很可能有没注意到的BUG,欢迎指出。
比较排序和非比较排序
常见的排序算法都是比较排序,非比较排序包括计数排序、桶排序和基数排序,非比较排序对数据有要求,因为数据本身包含了定位特征,所有才能不通过比较来确定元素的位置。
比较排序的时间复杂度通常为O(n2)或者O(nlogn),比较排序的时间复杂度下界就是O(nlogn),而非比较排序的时间复杂度可以达到O(n),但是都需要额外的空间开销。
比较排序时间复杂度为O(nlogn)的证明:
a1,a2,a3……an序列的所有排序有n!种,所以满足要求的排序a1',a2',a3'……an'(其中a1'<=a2'<=a3'……<=an')的概率为1/n!。基于输入元素的比较排序,每一次比较的返回不是0就是1,这恰好可以作为决策树的一个决策将一个事件分成两个分支。比如冒泡排序时通过比较a1和a2两个数的大小可以把序列分成a1,a2……an与a2,a1……an(气泡a2上升一个身位)两种不同的结果,因此比较排序也可以构造决策树。根节点代表原始序列a1,a2,a3……an,所有叶子节点都是这个序列的重排(共有n!个,其中有一个就是我们排序的结果a1',a2',a3'……an')。如果每次比较的结果都是等概率的话(恰好划分为概率空间相等的两个事件),那么二叉树就是高度平衡的,深度至少是log(n!)。
又因为 1. n! < nn ,两边取对数就得到log(n!)<nlog(n),所以log(n!) = O(nlogn).
2. n!=n(n-1)(n-2)(n-3)…1 > (n/2)^(n/2) 两边取对数得到 log(n!) > (n/2)log(n/2) = Ω(nlogn),所以 log(n!) = Ω(nlogn)。
因此log(n!)的增长速度与 nlogn 相同,即 log(n!)=Θ(nlogn),这就是通用排序算法的最低时间复杂度O(nlogn)的依据。
排序的稳定性和复杂度
不稳定:
选择排序(selection sort)— O(n2)
快速排序(quicksort)— O(nlogn) 平均时间, O(n2) 最坏情况; 对于大的、乱序串列一般认为是最快的已知排序
堆排序 (heapsort)— O(nlogn)
希尔排序 (shell sort)— O(nlogn)
基数排序(radix sort)— O(n·k); 需要 O(n) 额外存储空间 (K为特征个数)
稳定:
插入排序(insertion sort)— O(n2)
冒泡排序(bubble sort) — O(n2)
归并排序 (merge sort)— O(n log n); 需要 O(n) 额外存储空间
二叉树排序(Binary tree sort) — O(nlogn); 需要 O(n) 额外存储空间
计数排序 (counting sort) — O(n+k); 需要 O(n+k) 额外存储空间,k为序列中Max-Min+1
桶排序 (bucket sort)— O(n); 需要 O(k) 额外存储空间
每种排序的原理和实现
插入排序
遍历数组,遍历到i时,a0,a1...ai-1是已经排好序的,取出ai,从ai-1开始向前和每个比较大小,如果小于,则将此位置元素向后移动,继续先前比较,如果不小于,则放到正在比较的元素之后。可见相等元素比较是,原来靠后的还是拍在后边,所以插入排序是稳定的。
当待排序的数据基本有序时,插入排序的效率比较高,只需要进行很少的数据移动。
void insertion_sort (int a[], int n) { int i,j,v; for (i=1; i<n; i++) {
//如果第i个元素小于第j个,则第j个向后移动 for (v=a[i], j=i-1; j>=0&&v<a[j]; j--) a[j+1]=a[j]; a[j+1]=v; } }
选择排序
遍历数组,遍历到i时,a0,a1...ai-1是已经排好序的,然后从i到n选择出最小的,记录下位置,如果不是第i个,则和第i个元素交换。此时第i个元素可能会排到相等元素之后,造成排序的不稳定。
void selection_sort (int a[], int n) { int i,j,pos,tmp; for (i=0; i<n-1; i++) {
//寻找最小值的下标 for (pos=i, j=i+1; j<n; j++) if (a[pos]>a[j]) pos=j; if (pos != i) { tmp=a[i]; a[i]=a[pos]; a[pos]=tmp; } } }
冒泡排序
冒泡排序的名字很形象,实际实现是相邻两节点进行比较,大的向后移一个,经过第一轮两两比较和移动,最大的元素移动到了最后,第二轮次大的位于倒数第二个,依次进行。这是最基本的冒泡排序,还可以进行一些优化。
优化一:如果某一轮两两比较中没有任何元素交换,这说明已经都排好序了,算法结束,可以使用一个Flag做标记,默认为false,如果发生交互则置为true,每轮结束时检测Flag,如果为true则继续,如果为false则返回。
优化二:某一轮结束位置为j,但是这一轮的最后一次交换发生在lastSwap的位置,则lastSwap到j之间是排好序的,下一轮的结束点就不必是j--了,而直接到lastSwap即可,代码如下:
void bubble_sort (int a[], int n) { int i, j, lastSwap, tmp; for (j=n-1; j>0; j=lastSwap) { for (i=0; i<j; i++) { if (a[i] > a[i+1]) { tmp=a[i]; a[i]=a[i+1]; a[i+1]=tmp; //最后一次交换位置的坐标 lastSwap = i; } } } }
快速排序
快速排序首先找到一个基准,下面程序以第一个元素作为基准(pivot),然后先从右向左搜索,如果发现比pivot小,则和pivot交换,然后从左向右搜索,如果发现比pivot大,则和pivot交换,一直到左边大于右边,此时pivot左边的都比它小,而右边的都比它大,此时pivot的位置就是排好序后应该在的位置,此时pivot将数组划分为左右两部分,可以递归采用该方法进行。快排的交换使排序成为不稳定的。
int mpartition(int a[], int l, int r) { int pivot = a[l]; while (l<r) { while (l<r && pivot<=a[r]) r--; if (l<r) a[l++]=a[r]; while (l<r && pivot>=a[l]) l++; if (l<r) a[r--]=a[l]; } a[l]=pivot; return l; } void quick_sort (int a[], int l, int r) { if (l < r) { int q = mpartition(a, l, r); msort(a, l, q-1); msort(a, q+1, r); } }
堆排序
堆排序是把数组看作堆,第i个结点的孩子结点为第2*i+1和2*i+2个结点(不超出数组长度前提下),堆排序的第一步是建堆,然后是取堆顶元素然后调整堆。建堆的过程是自底向上不断调整达成的,这样当调整某个结点时,其左节点和右结点已经是满足条件的,此时如果两个子结点不需要动,则整个子树不需要动,如果调整,则父结点交换到子结点位置,再以此结点继续调整。
下述代码使用的大顶堆,建立好堆后堆顶元素为最大值,此时取堆顶元素即使堆顶元素和最后一个元素交换,最大的元素处于数组最后,此时调整小了一个长度的堆,然后再取堆顶和倒数第二个元素交换,依次类推,完成数据的非递减排序。
堆排序的主要时间花在初始建堆期间,建好堆后,堆这种数据结构以及它奇妙的特征,使得找到数列中最大的数字这样的操作只需要O(1)的时间复杂度,维护需要logn的时间复杂度。堆排序不适宜于记录数较少的文件
void heapAdjust(int a[], int i, int nLength) { int nChild; int nTemp; for (nTemp = a[i]; 2 * i + 1 < nLength; i = nChild) { // 子结点的位置=2*(父结点位置)+ 1 nChild = 2 * i + 1; // 得到子结点中较大的结点 if ( nChild < nLength-1 && a[nChild + 1] > a[nChild]) ++nChild; // 如果较大的子结点大于父结点那么把较大的子结点往上移动,替换它的父结点 if (nTemp < a[nChild]) { a[i] = a[nChild]; a[nChild]= nTemp; } else // 否则退出循环 break; } } // 堆排序算法 void heap_sort(int a[],int length) { int tmp; // 调整序列的前半部分元素,调整完之后第一个元素是序列的最大的元素 //length/2-1是第一个非叶节点,此处"/"为整除 for (int i = length / 2 - 1; i >= 0; --i) heapAdjust(a, i, length); // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素 for (int i = length - 1; i > 0; --i) { // 把第一个元素和当前的最后一个元素交换, // 保证当前的最后一个位置的元素都是在现在的这个序列之中最大的 /// Swap(&a[0], &a[i]); tmp = a[i]; a[i] = a[0]; a[0] = tmp; // 不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值 heapAdjust(a, 0, i); } }
归并排序
归并排序是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。这需要将待排序序列中的所有记录扫描一遍,因此耗费O(n)时间,而由完全二叉树的深度可知,整个归并排序需要进行.logn.次,因此,总的时间复杂度为O(nlogn)。
归并排序在归并过程中需 要与原始记录序列同样数量的存储空间存放归并结果,因此空间复杂度为O(n)。
归并算法需要两两比较,不存在跳跃,因此归并排序是一种稳定的排序算法。
void mergearray(int a[], int first, int mid, int last, int temp[]) { int i = first, j = mid + 1; int m = mid, n = last; int k = 0; while (i <= m && j <= n) { if (a[i] <= a[j]) temp[k++] = a[i++]; else temp[k++] = a[j++]; } while (i <= m) temp[k++] = a[i++]; while (j <= n) temp[k++] = a[j++]; for (i = 0; i < k; i++) a[first + i] = temp[i]; } void merge_sort(int a[], int first, int last, int temp[]) { if (first < last) { int mid = (first + last) / 2; merge_sort(a, first, mid, temp); //左边有序 merge_sort(a, mid + 1, last, temp); //右边有序 mergearray(a, first, mid, last, temp); //再将二个有序数列合并 } }
有的地方看到在mergearray()合并有序数列时分配临时数组,即每一步mergearray的结果存放的一个新的临时数组里,这样会在递归中消耗大量的空间。因此做出小小的变化。只需要new一个临时数组。后面的操作都共用这一个临时数组。合并完后将临时数组中排好序的部分写回原数组。
归并排序计算时间复杂度时可以很容易的列出递归方程,也是计算时间复杂度的一种方法。
希尔排序
希尔排序是对插入排序的优化,基于以下两个认识:1. 数据量较小时插入排序速度较快,因为n和n2差距很小;2. 数据基本有序时插入排序效率很高,因为比较和移动的数据量少。
因此,希尔排序的基本思想是将需要排序的序列划分成为若干个较小的子序列,对子序列进行插入排序,通过则插入排序能够使得原来序列成为基本有序。这样通过对较小的序列进行插入排序,然后对基本有序的数列进行插入排序,能够提高插入排序算法的效率。
希尔排序的划分子序列不是像归并排序那种的二分,而是采用的叫做增量的技术,例如有十个元素的数组进行希尔排序,首先选择增量为10/2=5,此时第1个元素和第(1+5)个元素配对成子序列使用插入排序进行排序,第2和(2+5)个元素组成子序列,完成后增量继续减半为2,此时第1个元素、第(1+2)、第(1+4)、第(1+6)、第(1+8)个元素组成子序列进行插入排序。这种增量选择方法的好处是可以使数组整体均匀有序,尽可能的减少比较和移动的次数,二分法中即使前一半数据有序,后一半中如果有比较小的数据,还是会造成大量的比较和移动,因此这种增量的方法和插入排序的配合更佳。
希尔排序的时间复杂度和增量的选择策略有关,上述增量方法造成希尔排序的不稳定性。
void shell_sort(int a[], int n) { int d, i, j, temp; //d为增量 for(d = n/2;d >= 1;d = d/2) //增量递减到1使完成排序 { for(i = d; i < n;i++) //插入排序的一轮 { temp = a[i]; for(j = i - d;(j >= 0) && (a[j] > temp);j = j-d) { a[j + d] = a[j]; } a[j + d] = temp; } } }
二叉树排序
二叉树排序法借助了数据结构二叉排序树,二叉排序数满足三个条件:(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值; (2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值; (3)左、右子树也分别为二叉排序树。根据这三个特点,用中序遍历二叉树得到的结果就是排序的结果。
二叉树排序法需要首先根据数据构建二叉排序树,然后中序遍历,排序时间复杂度为O(nlogn),构建二叉树需要额外的O(n)的存储空间,有相同的元素是可以设置排在后边的放在右子树,在中序变量的时候也会在后边,所以二叉树排序是稳定的。
在实现此算法的时候遇到不小的困难,指针参数在函数中无法通过new赋值,后来采用取指针地址,然后函数设置BST** tree的方式解决。
int arr[] = {7, 8, 8, 9, 5, 16, 5, 3,56,21,34,15,42}; struct BST{ int number; //保存数组元素的值 struct BST* left; struct BST* right; }; void insertBST(BST** tree, int v) { if (*tree == NULL) { *tree = new BST; (*tree)->left=(*tree)->right=NULL; (*tree)->number=v; return; } if (v < (*tree)->number) insertBST(&((*tree)->left), v); else insertBST(&((*tree)->right), v); } void printResult(BST* tree) { if (tree == NULL) return; if (tree->left != NULL) printResult(tree->left); cout << tree->number << " "; if (tree->right != NULL) printResult(tree->right); } void createBST(BST** tree, int a[], int n) { *tree = NULL; for (int i=0; i<n; i++) insertBST(tree, a[i]); } int main() { int n = sizeof(arr)/sizeof(int); BST* root; createBST(&root, arr, n); printResult(root); }
计数排序
如果通过比较进行排序,那么复杂度的下界是O(nlogn),但是如果数据本身有可以利用的特征,可以不通过比较进行排序,就能使时间复杂度降低到O(n)。
计数排序要求待排序的数组元素都是 整数,有很多地方都要去是0-K的正整数,其实负整数也可以通过都加一个偏移量解决的。
计数排序的思想是,考虑待排序数组中的某一个元素a,如果数组中比a小的元素有s个,那么a在最终排好序的数组中的位置将会是s+1,如何知道比a小的元素有多少个,肯定不是通过比较去觉得,而是通过数字本身的属性,即累加数组中最小值到a之间的每个数字出现的次数(未出现则为0),而每个数字出现的次数可以通过扫描一遍数组获得。
计数排序的步骤:
- 找出待排序的数组中最大和最小的元素(计数数组C的长度为max-min+1,其中位置0存放min,依次填充到最后一个位置存放max)
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1(反向填充是为了保证稳定性)
以下代码中寻找最大和最小元素参考编程之美,比较次数为1.5n次。
计数排序适合数据分布集中的排序,如果数据太分散,会造成空间的大量浪费,假设数据为(1,2,3,1000000),这就需要1000000的额外空间,并且有大量的空间浪费和时间浪费。
void findArrMaxMin(int a[], int size, int *min, int *max) { if(size == 0) { return; } if(size == 1) { *min = *max = a[0]; return; } *min = a[0] > a[1] ? a[1] : a[0]; *max = a[0] <= a[1] ? a[1] : a[0]; int i, j; for(i = 2, j = 3; i < size, j < size; i += 2, j += 2) { int tempmax = a[i] >= a[j] ? a[i] : a[j]; int tempmin = a[i] < a[j] ? a[i] : a[j]; if(tempmax > *max) *max = tempmax; if(tempmin < *min) *min = tempmin; } //如果数组元素是奇数个,那么最后一个元素在分组的过程中没有包含其中, //这里单独比较 if(size % 2 != 0) { if(a[size -1] > *max) *max = a[size - 1]; else if(a[size -1] < *min) *min = a[size -1]; } } void count_sort(int a[], int b[], int n) { int max, min; findArrMaxMin(a, n, &min, &max); int numRange = max-min+1; int* counter = new int[numRange]; int i, j, k; for (k=0; k<numRange; k++) counter[k]=0; for (i=0; i<n; i++) counter[a[i]-min]++; for (k=1; k<numRange; k++) counter[k] += counter[k-1]; for (j=n-1; j>=0; j--) { int v = a[j]; int index = counter[v-min]-1; b[index]=v; counter[v-min]--; } }
桶排序
假设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]....B[M]中的全部内容即是一个有序序列。
桶排序利用函数的映射关系,减少了计划所有的比较操作,是一种Hash的思想,可以用在海量数据处理中。
我觉得计数排序也可以看作是桶排序的特例,数组关键字范围为N,划分为N个桶。
基数排序
基数排序也可以看作一种桶排序,不断的使用不同的标准对数据划分到桶中,最终实现有序。基数排序的思想是对数据选择多种基数,对每一种基数依次使用桶排序。
基数排序的步骤:以整数为例,将整数按十进制位划分,从低位到高位执行以下过程。
1. 从个位开始,根据0~9的值将数据分到10个桶桶,例如12会划分到2号桶中。
2. 将0~9的10个桶中的数据顺序放回到数组中。
重复上述过程,一直到最高位。
上述方法称为LSD(Least significant digital),还可以从高位到低位,称为MSD。
int getNumInPos(int num,int pos) //获得某个数字的第pos位的值 { int temp = 1; for (int i = 0; i < pos - 1; i++) temp *= 10; return (num / temp) % 10; } #define RADIX_10 10 //十个桶,表示每一位的十个数字 #define KEYNUM 5 //整数位数 void radix_sort(int* pDataArray, int iDataNum) { int *radixArrays[RADIX_10]; //分别为0~9的序列空间 for (int i = 0; i < RADIX_10; i++) { radixArrays[i] = new int[iDataNum]; radixArrays[i][0] = 0; //index为0处记录这组数据的个数 } for (int pos = 1; pos <= KEYNUM; pos++) //从个位开始到31位 { for (int i = 0; i < iDataNum; i++) //分配过程 { int num = getNumInPos(pDataArray[i], pos); int index = ++radixArrays[num][0]; radixArrays[num][index] = pDataArray[i]; } for (int i = 0, j =0; i < RADIX_10; i++) //写回到原数组中,复位radixArrays { for (int k = 1; k <= radixArrays[i][0]; k++) pDataArray[j++] = radixArrays[i][k]; radixArrays[i][0] = 0; } } }