Sorting
【原文见:http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=sorting】
作者 By timmac
TopCoder Member
翻译 农夫三拳@seu
drizzlecrj@gmail.com
许多实际应用的软件在计算时都需要计算的对象是有序的。甚至在开始计算之前,排序的重要性已经摆在我们面前了。从在集体照相的时候需要最高的人站在后面到在圣诞节获得最高收益的商人,把对象按从小到大或者从第一个到最后一个进行排列的需要不能被忽视。
当我们查询一个数据库的时候,在后面加上一个ORDER BY的子句,其实就是排序。当我们在电话簿中找一个条款时,我们处理的是一个已经排好序的列表。(想想看如果没有排序的话!)如果你需要使用二分查找在一个数组中高效查找,首先将数组进行排序是很必要的。当一个问题要求我们在答案相同的情况下按照字典序返回第一个结果,好的...,你知道这个意思了。
General Considerations
考虑有一群人,给他们每一个人一叠打乱过的扑克牌,并且需要它们将这些扑克牌以升序的方式进行排序。一些人可能开始的时候放成一堆堆的,而其他人可能将扑克牌在桌子上铺开,而仍然有一些人在他们的手里玩这些扑克牌。这个练习可能需要几秒,对某些人可能几分钟或者更长时间。一些人在完成之后的扑克牌中,黑桃的总是在红桃之前,在其他的人当中或许根本没有组织。基本上,这些就是算法家们争论的不同排序算法的正反面的要点。
当比较不同的排序算法时,需要考虑许多事情。第一个通常是运行时间。当处理不断增长的大量数据时,应用中实际使用的低效算法通常会运行的很慢。
第二个要考虑的是内存使用。快速的算法通常需要递归调用,不可避免的包含了待排序的数据的拷贝。在某些环境下,内存空间可能很重要(比如在嵌入式系统中),特定的算法可能并不实际。在某些情况下,也许可以将算法修改成“在原地”进行的,而不需要创建数据的拷贝。尽管如此,这个修改也许会损失一些性能。
第三个要考虑的是稳定性。稳定性,简单的定义,元素的相对位置不变。在一个稳定的排序中,那些比较关键字相同的元素在排序后的相对位置和排序前是一样的。在一个不稳定排序中,那些比较关键字相同的元素不能保证排序后的相对位置。
Bubble Sort
第一个教给学生的排序算法是冒泡排序。虽然它在实用中对一些较小的数据集合比较快而并不是对于所有的都快,但是它能够展示一个排序算法是怎样工作的。一般来说,它看起来像下面这样:
for (int i = 0; i < data.Length; i++)
for (int j = 0; j < data.Length - 1; j++)
if (data[j] > data[j + 1])

{
tmp = data[j];
data[j] = data[j + 1];
data[j + 1] = tmp;
}
思路是从数据的一端到另外一端,通过当第一个数字大于后一个的时候,交换两个相邻的元素。因此,最小的元素将会“冒泡”到表面。这个算法是O(n^2)的运行时,因此对于大的数据集合很慢。冒泡排序最好的优点,是它非常易于理解并且编码。此外,它是一个稳定的排序,不需要额外的内存,因为所有的交换都在原地进行的。
Insertion Sort
插入排序是一个每次将一个元素进行排序的算法。在每一次迭代的过程中,它取得待排序的元素,然后将它放到那些已经排好序的数中合适的位置。

for (int i = 0; i <= data.Length; i++)
{
int j = i;
while (j > 0 && data[i] < data[j - 1])
j--;
int tmp = data[i];
for (int k = i; k > j; k--)
data[k] = data[k - 1];
data[j] = tmp;
}

外层循环每次循环的数据应该像下面这样:


{18, 6, 9, 1, 4, 15, 12, 5, 6, 7, 11}


{ 6, 18, 9, 1, 4, 15, 12, 5, 6, 7, 11}


{ 6, 9, 18, 1, 4, 15, 12, 5, 6, 7, 11}


{ 1, 6, 9, 18, 4, 15, 12, 5, 6, 7, 11}


{ 1, 4, 6, 9, 18, 15, 12, 5, 6, 7, 11}


{ 1, 4, 6, 9, 15, 18, 12, 5, 6, 7, 11}


{ 1, 4, 6, 9, 12, 15, 18, 5, 6, 7, 11}


{ 1, 4, 5, 6, 9, 12, 15, 18, 6, 7, 11}


{ 1, 4, 5, 6, 6, 9, 12, 15, 18, 7, 11}


{ 1, 4, 5, 6, 6, 7, 9, 12, 15, 18, 11}


{ 1, 4, 5, 6, 6, 7, 9, 11, 12, 15, 18}
插入排序的一个优点是它对于初始几乎排好序的列表非常高效。更进一步的,它能够工作在不断增加元素的集合上。例如,如果一个人想要维护一个比赛中达到的最高分的排好序的列表,插入排序这是将会工作的很好,因为随着比赛的进行,新的元素将会加入到元素中。
Merge Sort
归并排序是递归的运行的。首先将数据集合划分成一半,然后分别对每一部分进行排序。然后,两个列表的第一个元素将进行比较。较小的元素从它的列表中移除并加入到最终结果的列表中。

int[] mergeSort (int[] data)
{
if (data.Length == 1)
return data;
int middle = data.Length / 2;
int[] left = mergeSort(subArray(data, 0, middle - 1));
int[] right = mergeSort(subArray(data, middle, data.Length - 1));
int[] result = new int[data.Length];
int dPtr = 0;
int lPtr = 0;
int rPtr = 0;

while (dPtr < data.Length)
{

if (lPtr == left.Length)
{
result[dPtr] = right[rPtr];
rPtr++;

} else if (rPtr == right.Length)
{
result[dPtr] = left[lPtr];
lPtr++;

} else if (left[lPtr] < right[rPtr])
{
result[dPtr] = left[lPtr];
lPtr++;

} else
{
result[dPtr] = right[rPtr];
rPtr++;
}
dPtr++;
}
return result;
}

每一个递归调用是O(n)的运行时,并且需要O(logn)次的递归调用,因此这个算法的时间复杂度为O(n*logn)。归并排序同样在初始将近排好序的列表上有着很好的性能。在对每一个部分进行排序后,如果一个列表中的最大元素小于另外一半的最小元素,那么合并的步骤就不需要了。(例如,Java API实现了这个特殊的优化)数据,随着过程不断的递归调用,可能像下面这样:


{18, 6, 9, 1, 4, 15, 12, 5, 6, 7, 11}


{18, 6, 9, 1, 4}
{15, 12, 5, 6, 7, 11}


{18, 6}
{9, 1, 4}
{15, 12, 5}
{6, 7, 11}


{18}
{6}
{9}
{1, 4}
{15}
{12, 5}
{6}
{7, 11}


{18}
{6}
{9}
{1}
{4}
{15}
{12}
{5}
{6}
{7}
{11}


{18}
{6}
{9}
{1, 4}
{15}
{5, 12}
{6}
{7, 11}


{6, 18}
{1, 4, 9}
{5, 12, 15}
{6, 7, 11}


{1, 4, 6, 9, 18}
{5, 6, 7, 11, 12, 15}


{1, 4, 5, 6, 6, 7, 9, 11, 12, 15, 18}
除了很高效之外,归并排序具有另外一个优点,它可以用来解决其他问题,例如决定一个给定列表的“没排序”程度。
Heap Sort
在堆排序中,我们创建了一个堆数据结构。堆是用来以树的形式进行数据存储的数据结构,最小的元素总是在根节点处。
(堆,和优先队列一样,在Data Structures中讲述了它们更多的细节。)为了进行堆排序,所有的列表中的数据都被插入到了堆当中,并且根节点的元素不断的被移除,并放在列表的最后。由于根节点总是最小的元素,因此结果是一个排好序的列表。如果你已经有了一个可用的堆实现或者你使用Java PriorityQueue(1.5版本中可用的),执行一个堆排序的代码很短:
Heap h = new Heap();
for (int i = 0; i < data.Length; i++)
h.Add(data[i]);
int[] result = new int[data.Length];
for (int i = 0; i < data.Length; i++)
data[i] = h.RemoveLowest();
堆排序的上界范围是O(n*logn)。存储空间仅需要存储堆的部分;这个大小和列表的大小成线性关系。堆排序的缺点是不稳定,并且它在基本实现中稍微有点复杂且不易理解。
Quick Sort
快速排序,正如它的名字,是一个高效的排序算法。它背后的理论和人类排序的方式很接近。首先,将书籍分为两个组,“高”
值和“低”值。然后,递归处理这两半。最后重新组合成现在的排序好的列表。

Array quickSort(Array data)
{
if (Array.Length <= 1)
return;
middle = Array[Array.Length / 2];
Array left = new Array();
Array right = new Array();
for (int i = 0; i < Array.Length; i++)

if (i != Array.Length / 2)
{
if (Array[i] <= middle)
left.Add(Array[i]);
else
right.Add(Array[i]);
}
return concatenate(quickSort(left), middle, quickSort(right));
}
快速排序的挑战是为划分成两个组而挑选的合理的“中间”值。算法的效率完全依赖于成功找到一个准确的中值。在最好的情况下,这个算法的时间复杂度为O(n*logn)。在最坏的情况下,两个分组中总是有一个只要一个元素,运行时就变为了O(n^2)。真实排序的元素可像下面这样工作:


{18, 6, 9, 1, 4, 15, 12, 5, 6, 7, 11}

![]()