该文档为学习基本排序算法过程中的学习笔记,大部分内容从网络上其他渠道也能得到,仅用于记录备忘之用。
冒泡、选择、插入三种作为基本的排序算法是必须要掌握的,而在MapReduce的实际应用中。在Map阶段,k-v溢写时,采用的正是快排;而溢出文件的合并使用的则是归并;在Reduce阶段,通过shuffle从Map获取的文件进行合并的时候采用的也是归并;最后阶段则使用了堆排作最后的合并过程。
所以快排、归并以及堆排是必须要掌握的排序算法,这都在MapReduce内部使用的排序算法,学习Hadoop的必须过程。
所谓算法稳定性即能够保证排序前两个相等的数在排序中的过程中不会改变这两个数的顺序:例如Ai=Aj,Ai原来在Aj之前,但在排序之后Aj排在了Ai之前,这就是不稳定的表现。
不稳定的算法会导致元素交换增多(无效交换)。
在一个长度为N的无序数组中,在第一趟遍历N个数据,将最小的数值与第一个交换,第二趟遍历N-1次,将剩下中最小的与第二个元素交换...第N-1趟遍历剩下两个元素,判断大小交换位置即可,完成排序。
Ø 平均时间复杂度:O(n2);
Ø 空间复杂度:O(1); //用于交换和记录索引
Ø 稳定性:不稳定; //例如[5,5,3]在第一趟排序中,第一个5和3交换位置,破坏了稳定性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void
SelectionSort(
int
*pDataArray,
int
iDataNum) {
for
(
int
i = 0; i < iDataNum, i++) {
//从第一个元素开始
int
key = i;
//用于交换的索引
for
(
int
j = i + 1; j < iDataNum; j++) {
//i+1之前的元素已经排好序
if
(pDataArray[j] < pDataArray[key])
key = j;
//动态更新key索引,指向最小索引
}
if
(key != i) {
//若key和i不重叠,则交换
int
tmp = pDataArray[key];
//将最小值放在tmp中
pDataArray[key] = pDataArray[i];
//交换i值
pDataArray[i] = tmp;
//最小值放入i中,这一趟结束
}
}
}
|
属于基本排序,性能较差,较少使用。
长度为N的无序数组,第一堂从1到N,依次和旁边的比较,大数右移,最后将最大的那个值滚动到N位置;第二趟类似前面,将第二大的值放到N-1位...直到第N-1趟完成排序。整个过程类似一个水泡依次网上冒,直到冒到最大的位置上。
Ø 平均时间复杂度:O(n2);
Ø 空间复杂度:O(1); //用于交换的额外空间开销
Ø 稳定性:稳定;
1
2
3
4
5
6
7
8
9
10
11
|
void
BubbleSort(
int
*pDataArray,
int
iDataNum) {
for
(
int
i = 0; i < iDataNum - 1; i++) {
//必须进行N-1次的比较
for
(
int
j = 0 ; j < iDataNum - 1 - i; j++) {
//iDataNum - 1 - i之后的元素已经有序
if
(pDataArray[j] > pDataArray[j+1]) {
//相邻两数进行比较,若前大后小则进行交换
int
tmp = pDataArray[j];
pDataArray[j] = pDataArray[j+1];
pDataArrary[j+1] = tmp;
//完成交换
}
}
}
}
|
属于基本排序,性能较差,较少使用。
所谓插入排序即认为一个子序列是有序的,将一个数值插入到其中合适的位置中形成一个新的有序数列。长度为N的数组中,第一趟认为第一个数值是有序的,从第二个元素开始进行插入;第二趟从第三个元素插入...依次直到第N-1趟,第N个元素插入前面的有序数列完成排序。
Ø 平均时间复杂性:O(n2);
Ø 空间复杂度:O(1);
Ø 稳定性:稳定;
1
2
3
4
5
6
7
8
9
10
11
12
|
void
InsertSor(
int
*pDataArray,
int
iDataNum) {
for
(
int
i = 1; i < iDataNum; i++) {
//从第二个元素开始pDataArray[0]只有一个元素的数列,有序
int
j = i - 1;
//j为比较下标,从后往前比较,初始i-1
int
tmp = pDataArray[i];
//将预插元素放入tmp中
while
(j >= 0 && pDataArray[j] > tmp) {
pDataArray[j+1] = pDataArray[j];
//将元素往后移,第i个元素已经放入tmp,不会被覆盖
j--;
}
if
( j != i - 1)
//只要j改变了,则需要换位
pDataArray[j] = tmp;
//将元素插入合适的位置
}
}
|
//在查找的过程中,考虑是否可以用二分查找的方式查找插入位置,但时间复杂度不变
二分查找:
1
2
3
4
5
6
|
int
BinSearch(
int
*pDataArray,
int
begin,
int
end,
int
SearchData) {
int
mid = (begin + end)/2;
if
(pDataArray[mid] == SearchData)
return
mid;
else
if
(pDataArray[mid] < SearchData)
return
BinSearch(pDataArray, mid + 1, end, SearchData);
else
return
BinSearch(pDataArray, begin, mid -1, SearchData);
}
//采用递归的方式进行二分查找,减少比较次数
|
属于基本排序算法,性能较低,较少使用
采用分而治之的思想,将无序数组进行分割,选择一个元素value(通常是第一个元素),第一次将小于Value的放在前段,大于value的放在后端;第二次排序分别对两段进行重复如上操作...进行递归操作,直到粒度划分到最小两个元素。
Ø 平均时间复杂度:O(nlog2n);
Ø 空间复杂度:O(n);
Ø 稳定性:不稳定;
//采用类似二分查找的递归方式(也是分段)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
int
Split (
int
*pDataArray,
int
Begin,
int
End) {
//将数组以Value(begin元素)分成前后两段
int
key=pDataArray[Begin];
//以第一个元素begin作为key或者说value值
while
(Begin < End) {
//在交换位置的过程中,不断缩小Begin和End的差值,直到相等才结束
while
(Begin < End && pDataArray[End] >= key)
//从后查找小于key的值并且缩小end值
End--;
//找到小于key的那个元素停止
if
( Begin != End) {
pDataArray[Begin] = pDataArray[End];
//将查找到的那个end元素与begin交换,第一次交换其实是将key的元素位覆盖了,不过它已经放入key
Begin++;
//将Begin右移一位
while
(Begin < End && pDataArray[Begin] <= key)
Begin++;
//从左往右查找比key大的值,找到后停止
if
(Begin != End) {
pDataArray[End] = pDataArray[Begin];
End--;
//将查找到的Begin元素放到之前的那个End位置(End位置元素已经移走可覆盖)
}
}
}
pDataArray[Begin] = key;
//最终Begin=End,退出while,而Begin位为空,刚好把key填入
return
Begin;
}
//针对一次排序
|
1
2
3
4
5
6
7
8
9
10
11
|
Void QSort (
int
*pDataArray,
int
Begin,
int
End) {
//用于递归
if
(End > Begin) {
int
Mid = Split(pDataArray, Begin, End);
//获取折半位置
QSort (pDataArray, Begin, Mid - 1);
//以该位置将以value值划分的数组分两段分别进行递归
QSort (pDataArray, Mid + 1, End);
//这是后段
}
}
void
QuickSort (
int
*pDataArray,
int
iDataNum) {
//快排入口
QSort(pDataArray, 0, iDataNum - 1);
//初始位置0到iDataNum-1
}
|
比较常用的排序算法,Hadoop中Map阶段第一次排序默认使用的就是快排。
归并排序是将两个有序表合并成一个新的有序表,即把待排序的序列分成若干个有序的子序列,再把有序的子序列合并为整体有序序列。
而自底向上的归并则是将长度为N的无序数组切分成若干个N个有序子序列,再两两合并(起始时单元素为一个子序列),然后再将合并后的N/2(或者N/2+1)个子序列进行两两合并,依次类推得到一个完整的有序数组。
Ø 平均时间复杂度:O(nlog2n);
Ø 空间复杂度:O(n); //用于存储有序子序列合并后的有序子序列
Ø 稳定性:稳定
//自底向上的归并
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
void
Merge (
int
*pDataArray, *
int
pTempArray,
int
bIndex,
int
mIndex,
int
eIndex) {
int
mLenth = eIndex - bIndex;
//合并后的有序序列长度
int
i = 0;
//记录合并后插入数据的偏移
int
j = bIndex;
//记录子序列1插入数据的偏移
int
k = mIndex;
//记录子序列2插入数据的偏移(初始mIndex为两个序列的中间位置,子序列1尾,2的首)
while
(j < mIndex && k < eIndex) {
//只要两者的偏移不超过自身子序列的长度即可
if
(pDataArray[j] <= pDataArray[k]) {
pTempArray[i++] = pDataArray[j];
//子序列小,则将对应数据插入临时数组
j++;
//将子序列1继续偏移
}
else
{
pTemArray[i++] = pDataArray[k];
k++;
//两种情况,要么插子序列1,要么子序列2
}
if
(j == mIndex)
//说明子序列1已经插入完毕,但还剩下有部分子序列2未插入
while
(k < eIndex)
pTempArray[i++] = pDataArray[k++];
//将剩下的子序列2插入
if
(k == eIndex)
//说明子序列2已经插入完毕,但还剩下有部分子序列1未插入
while
(j < eIndex)
pTempArray[i++] = pDataArray[j++];
//将剩下的子序列1插入
if
(i = 0; i < mLenth; i++)
//将合并后的数组重新放入pDataArray
pDataArray[bIndex+i] = pTempArray[i];
//注意pDataArray的起始位置是bIndex
}
}
//只是完成两个有序子序列的排序
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void
BottomUpMergeSort (
int
*pDataArray,
int
iDataNum) {
int
*pTempArray = (
int
*)
malloc
(
sizeof
(
int
) * iDataNum);
//临时存放合并后的有序序列
int
length = 1;
//初始子序列的长度为1,都为有序(单元素)
while
(length < iDataNum) {
//子序列不能大于整个无序数组的长度
int
i = 0;
for
(; i + 2*length < iDataNum; i += 2*length)
Merge(pDataArray, pTempArray, i, i + length, i + 2*length);
//子序列的长度成倍数增长,1-->2-->4-->8,注意i的增长规律
if
(i + length < iDataNum)
Merge(pDataArray, pTempArray, i, i + length, iDataNum);
//最后一部分不成倍数的末尾部分(从i+length到iDataNum),直接归并
length *= 2;
//子序列的长度增长规律,2倍增长
}
free
(pTempArray);
//释放内存
}
|
在MapReduce的Reduce溢出文件Merge的过程中,默认使用的就是归并排序,将Map结果合并,所以掌握归并排序至关重要。
Ø 先把长度为N的数组调整成N个节点的组成的大顶推(若是降序排则调整为小顶堆),即根节点大于左右子树的完全二叉树。
Ø 将堆顶元素(对大值)与最后一个元素N交换,这样就形成了1~(N-1)以及N两个序列,一个是无序的,另一个是有序的。
Ø 将N-1个元素的新堆重新调整为大顶堆(为了再次找出最大的那个值),然后堆顶元素再次与最后一个元素(第N-1位)交换位置,则又形成了一个新的堆和一个新的有序数组(第N个元素和第N-1个元素)。
Ø 依次按照路上步骤进行操作,直到有序数组长度为N-1,则堆的size为1,即留下最后一个最小的值。至此,排序完成(从后往前排)。
Ø 平均时间复杂度:O(nlog2n);
Ø 空间复杂度:O(1);
Ø 稳定性:不稳定;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Void HeapAdjust (
int
*pDataArray,
int
i,
int
iDataNum) {
//堆调整函数
int
ichild = 2*i;
//i的左子树
int
rchild = 2*i + 1;
//i的右子树
int
max = i ;
//用于存储最大值,随时调整
if
(lchild <= iDataNum && pDataArray[lchild] > pDataArray[max]) {
//lchild不超范围且大于堆顶
max = lchild;
//进行最大值标记
}
if
(rchild <= iDataNum && pDataArray[rchild] > pDataArray[max]) {
//root依次和左子树,右子树比较,三者中找出最大值,进行max标记
max = rchild;
}
if
(max != i) {
//i不等于max说明,左右子树中存在大于root的节点
swap(pDataArray[i], pDataArray[max]);
//节点值进行交换
HeapAdjust(pDataArray, max, iDataNum);
//存在交换则进行递归,不过root切换为之前的max
}
}
//函数执行一次只进行一次交换(排除递归),进行递归的话则顺着max值往下走,直到形成大顶堆
|
1
2
3
4
5
6
7
8
9
10
11
|
Void HeapSort (
int
*pDataArray,
int
iDataNum) {
//堆排入口函数
int
i;
//用于初始化大顶堆
if
(i = iDataNum/2; i >= 1; i--) {
//注意pDataArray的位置是从1开始的
HeapAdjust (pDataArray, i, iDataNum);
//iDataNum/2为最后一个非叶子节点(可以研究下完全二叉树的特点),依次(i--)从非叶子节点开始构造,第一次只有三个节点,进行一次HeapAdjust之后形成一个三节点的大顶堆,最后形成了一个大顶堆
}
for
(i = iDataNum; i >= 1; i--) {
//i初始时是最后一个元素,所以为iDataNum
swap(pDataArray[1], pDataArray[i]);
//第一个元素和最后一个交换
HeapAdjust(pDataArray, 1, i - 1);
//交换元素之后进行堆调整
}
}
|
在数据量比较大的时候经常会使用堆排序进行数据排序,这是一种比较常用的排序算法。在MapReduce的内部实现中,在Reduce阶段最后文件合并的过程,即使用堆排序进行文件内部数据排序。
Read(读取) ==> Collect(生成K-V) ==> Spill(溢写)
Read: 从HDFS读取inputSplit(由InputFormat根据文件生成);
Collect:分为map过程和partition过程,map根据inputSplit生成KV对,而Partition添加分区标记(辅助排序用),并写入环形缓存区;
Spill: 分为sort过程、comparess过程以及combine过程。数据不断的写入环形缓存区,达到阈值之后开始溢写,在溢写的过程中进行一次Sort,这里使用的排序是快排(QuickSort);一次溢出生成一个file,并且在生成file的过程中进行压缩(compress);多个file又会进行一次文件合并,在文件合并的过程中进行排序,这里使用的排序是归并排序(MegerSort)。
Shuffle阶段主要就是一个数据拷贝的过程,Map端合成的大文件之后,通过HTTP服务(jetty server)拷贝到Reduce端。
拷贝到Reduce端的数据并不是马上写入文件,而是同样放在缓存中,达到阈值则进行溢写。
合并溢写生成的file,这里使用的排序为归并排序(MegerSort),生成一些更大的文件(进一步减少文件个数)。
在归并之后留下少量的大文件,最后对大文件进行一次最终合并,合并成一个有序的大文件(只有一个),这里使用的排序算法为堆排序(HeapSort)。
如上可以看到,一个MapReduce过程涉及到了一次快排、两次归并以及一次堆排的操作。
因此在学习Hadoop的过程中,掌握这些基本的排序算法还是非常有用的。
从第三章我们可以看出掌握快排、归并以及堆排对深度理解MapReduce的过程至关重要。而插入排序、冒泡排序以及选择排序则作为最基本的排序算法更是应更掌握的。