实践证明快速排序效率最高,数量大时,堆排序与快速排序都很快,堆排序会略微快一点,冒泡效率经常是最差的。
.net自带的排序非常非常非常强大,大数量时比堆快很多。
下面是10W个数字排序的结果:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Sort
{
class Program
{
static int radLen = new Random().Next( 5 , 20 );
private delegate int [] SortMethod( int [] arr);
static void Main( string [] args)
{
int [] array = new int [radLen];
Console.WriteLine( " 未排序数组: " );
for ( int i = 0 ; i < radLen; i ++ )
{
int n = new Random(DateTime.Now.Millisecond).Next( 50 );
array[i] = n;
Console.Write(n);
if (i < radLen - 1 )
{
Console.Write( " , " );
}
System.Threading.Thread.Sleep( 10 );
}
Console.WriteLine();
Console.WriteLine();
// array = new int[] { 1,12,49,10,48,35 };
Sort s = new Sort();
TestMethod( " 插入排序: " , s.InsertSort, array);
TestMethod( " 折半查找插入排序: " , s.BIInsert, array);
TestMethod( " 希尔插入排序: " , s.ShellSort, array);
TestMethod( " 冒泡排序: " , s.BubbleSort, array);
TestMethod( " 快速排序: " , s.QuickSort, array);
TestMethod( " 选择排序: " , s.SimpleSelectSort, array);
TestMethod( " 堆排序: " , s.HeapSort, array);
Console.ReadLine();
}
static void TestMethod( string methodTip, SortMethod method, int [] arr)
{
Console.WriteLine(methodTip);
DateTime dtStart = DateTime.Now;
int [] ar = method(arr);
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine( " 耗时:{0} " , DateTime.Now - dtStart);
Console.ResetColor();
PrintArray(ar);
}
static void PrintArray( int [] array)
{
for ( int i = 0 ; i < radLen; i ++ )
{
Console.Write(array[i]);
if (i < radLen - 1 )
{
Console.Write( " , " );
}
}
Console.WriteLine();
}
}
// 所有算法都是升序
public class Sort
{
private int [] array;
private void CopyArray( int [] arr)
{
array = new int [arr.Length];
Array.Copy(arr, this .array, arr.Length);
}
#region 插入排序
// 插入排序,插入到有序数组里,先取出第一个数值,其为有序,插入时先和有序数组最后一个比,如果小则从后向前遍历
// 插入排序需要向后移动元素
public int [] InsertSort( int [] arr)
{
CopyArray(arr);
if (array != null )
{
// int t = array[0];
for ( int i = 1 ; i < array.Length; i ++ )
{
// 如果当前值比有序序列最后一个小,要从后向前找到一元素
// 该元素是有序序列中(从后向前)第一个比当前值小的
if (array[i] < array[i - 1 ])
{
int t = array[i];
// 在有序数组中从后向前找第一个比当前小的
// 并且找的过程中,把大于当前值的元素后移
int j = i - 1 ;
while (j >= 0 && array[j] > t)
{
array[j + 1 ] = array[j];
j -- ;
}
// 由于j--,j指向的是有序序列中(从后向前)第一个比当前值小的前一位
array[j + 1 ] = t;
}
}
}
return array;
}
// 折半插入排序,插入排序的变形,用折半查找到应插入的位置
public int [] BIInsert( int [] arr)
{
CopyArray(arr);
if (array != null )
{
int t = array[ 0 ];
for ( int i = 1 ; i < array.Length; i ++ )
{
if (array[i] < array[i - 1 ])
{
t = array[i];
// 二分查找,找第一个比
int low = 0 ;
int high = i - 1 ;
int mid = 0 ;
while (low <= high)
{
mid = (low + high) / 2 ;
if (array[mid] < t)
{
low = mid + 1 ;
}
else
{
high = mid - 1 ;
}
}
// 向后移动
int j = i - 1 ;
while (j > high)
{
array[j + 1 ] = array[j];
j -- ;
}
array[j + 1 ] = t;
}
}
}
return array;
}
// 和插入排序算法类似,只是原算法每次加1,这里的每次加dk,
// 可以认为原算法就是步长为1的shell排序
private void ShellInsert( int [] arr, int dk)
{
int t = arr[ 0 ];
for ( int i = dk; i < arr.Length; i += dk)
{
if (arr[i] < arr[i - dk])
{
t = arr[i];
int j = i - dk;
while (j >= 0 && arr[j] > t)
{
arr[j + dk] = arr[j];
j -= dk;
}
arr[j + dk] = t;
}
}
}
// 数组基本有序时,插入排序比较有效,shell思想是根据步长分组,
// 步子能踏到的分为一组,然后每组进行插入排序,
// 步长是个int数组,从大到小,最后一个值必须是1,
// 直到步长为1的执行完
public int [] ShellSort( int [] arr)
{
CopyArray(arr);
int [] dkArray = new int [] { 5 , 3 , 1 }; // 步长,步长的设计影响着排序的效率
if (array != null )
{
for ( int i = 0 ; i < dkArray.Length; i ++ )
{
ShellInsert(array, dkArray[i]);
}
}
return array;
}
#endregion
#region 冒泡
// 冒泡,交换相邻,大者向上冒
// 存在交换次数过多的问题,可以说实验果是效率最差的一种排序算法
public int [] BubbleSort( int [] arr)
{
CopyArray(arr);
if (array != null )
{
// 如果在冒的过程中没有交换发生,说明已经有序
bool exchange = true ;
// 外层循环定位已经排序过的值
for ( int i = array.Length - 1 ; i > 0 && exchange; i -- )
{
exchange = false ;
for ( int j = 0 ; j < i; j ++ )
{
if (array[j] > array[j + 1 ])
{
array[j] ^= array[j + 1 ];
array[j + 1 ] ^= array[j];
array[j] ^= array[j + 1 ];
exchange = true ;
}
}
}
}
return array;
}
#endregion
#region 快速排序
// pivot设置在第一位
private int Partition( int [] arr, int low, int high)
{
int t = arr[low]; // 中轴
while (low < high)
{
// 因为中轴设置在第一位,必须先从后向前遍历
// 从右边找从中轴小的
while (arr[high] >= t && low < high)
{
high -- ;
}
// 把high值赋到low上
// 这就是为什么中轴在第一位,要从后往前遍历的原因
// 如果从前往后,把low值赋给high,逻辑上就错了,因为第一次high还没参与比,不能被赋值
// 该算法用t保存中轴,也就是第一位,所以中轴位置上的值可以赋值
// 找到中轴在数组中应该放的位置后,再把t赋给该位置
// 很巧妙
arr[low] = arr[high];
// low的值已经是high值了,上个步骤是比中轴大才会换到low上
// 不用再次比较low上的值,让low向后移一步
if (low < high)
{
low ++ ;
}
// 从前向后找比中轴大的元素
while (arr[low] <= t && low < high)
{
low ++ ;
}
// 找到后把low值放在high上,
// 在从后往前找时,high值放在low上了,也就是说数组中存在两个high对应的值
// 要知道最后一个是无效的,现在把low放到high上,没有问题,
// low放在high上后,low的值在数组又出现两次,第一个值是无效的
// 下次从后往前循环时又会把high赋给low,这是个交替的过程
// 最后low位置与high位置相同时,这就是中轴应该的位置,
// 可以想像一下,在交替过程中,有一个无效的指针忽左忽右,最后小的都移到指针左边了,大的都移到指针右边
arr[high] = arr[low];
// 与上面low++一样,避免下次从后到前时的比较
if (low < high)
{
high -- ;
}
}
// low == high时,就是中轴的位置
arr[low] = t;
return low;
}
// pivot设置在最后一位
private int PartitionV2( int [] arr, int low, int high)
{
int pivot = arr[high];
while (low < high)
{
while (low < high && arr[low] <= pivot)
{
low ++ ;
}
arr[high] = arr[low];
while (low < high && arr[high] >= pivot)
{
high -- ;
}
arr[low] = arr[high];
}
arr[low] = pivot;
return low;
}
// 使用交换,原始作法,不用变量保存中轴,高低交换
// 不像上面,数组中有一个无效元素
private int PartitionV3( int [] arr, int low, int high)
{
// 中轴只作判断依据
int pivot = arr[low];
while (low < high)
{
while (low < high && arr[high] >= pivot)
{
high -- ;
}
if (arr[low] != arr[high])
{
arr[low] ^= arr[high];
arr[high] ^= arr[low];
arr[low] ^= arr[high];
}
while (low < high && arr[low] <= pivot)
{
low ++ ;
}
if (arr[low] != arr[high])
{
arr[low] ^= arr[high];
arr[high] ^= arr[low];
arr[low] ^= arr[high];
}
}
return low;
}
// 快速排序,以一个元素做中轴(可以是第一个,最后一个,或者中间某元素)
// 其他元素,小的放在中轴左边,大的放在中轴右边
// 再利用递归思想,把左右两边使用快速排序
// 实验证明,快速排序果然够快
private void QSort( int [] arr, int low, int high)
{
if (low < high)
{
int pivot = Partition(arr, low, high);
// 左边快速排序
QSort(arr, low, pivot - 1 );
// 右边快速排序
QSort(arr, pivot + 1 , high);
}
}
public int [] QuickSort( int [] arr)
{
CopyArray(arr);
if (array != null )
{
QSort(array, 0 , array.Length - 1 );
}
return array;
}
#endregion
#region 选择排序
// 简单选择排序,找第一个最小的放在前面,再从其他中找最小的放在其后,如此反复
public int [] SimpleSelectSort( int [] arr)
{
CopyArray(arr);
if (array != null )
{
for ( int i = 0 ; i < array.Length - 1 ; i ++ )
{
int min = array[i];
int minPos = i;
// 在后面的元素中选择出最小的
for ( int j = i + 1 ; j < array.Length; j ++ )
{
if (min > array[j])
{
min = array[j];
minPos = j;
}
}
if (i != minPos)
{
array[minPos] ^= array[i];
array[i] ^= array[minPos];
array[minPos] ^= array[i];
}
}
}
return array;
}
#endregion
#region 堆排序
// 调整数组指定范围的元素成大顶堆
private void HeapAdjust( int [] arr, int startPos, int endPos)
{
// 因为数组下标从0开始,所以最后非叶结点计算方式与书上不同,书上为lastPos/2
// 同理,叶结点的确定也相应变化
// 保存父结点
int parent = arr[startPos];
// 对父结点的左右孩子进行比较调整
for ( int i = startPos * 2 + 1 ; i <= endPos; i = i * 2 + 1 ) // i值初始是左孩子,左孩子一定会有,但是不一定有右孩子
{
// 判断是否有右孩子,如果有而且右孩子比左孩子大,则应比较右孩子和父结点
if (i + 1 <= endPos && (arr[i] < arr[i + 1 ]))
{
i ++ ; // i换成右孩子
}
// 如果父结点大于孩子,退出
if (parent >= arr[i])
{
break ;
}
// 父结点小于孩子,交换父结点和孩子
arr[startPos] = arr[i];
// 定位到孩子结点,可能是左孩子或右孩子
// 继续循环把大值下沉
startPos = i;
}
// 已经沉到底了,startPos就是大值应该在的地方
arr[startPos] = parent;
}
// 堆排序,构造大顶堆和调整堆,用的是同一方法HeapAdjust
// 构造时,从最后一个非叶结点向根调整
// 输入时,从根结点向叶结点调整
public int [] HeapSort( int [] arr)
{
CopyArray(arr);
if (array != null )
{
// 构造堆,从最后一个非叶结点开始向上调整
// HeapAdjust的最后一个参数保证下标不越界,作限定范围用,并不参与计算
for ( int i = array.Length / 2 - 1 ; i >= 0 ; i -- )
{
HeapAdjust(array, i, array.Length - 1 );
}
// 把堆顶元素和数组最后一个元素交换,这样大值就放在后面,然后再从根对所有结点非输出结点进行调整
for ( int i = 0 ; i < array.Length - 1 ; i ++ )
{
// 堆顶是最大元素,与最后元素交换
// 这样,最大的放在后面,而把最后元素放在堆顶,可以保证只对一个子树进行调整,这样调整影响降到最小
int t = array[ 0 ];
array[ 0 ] = array[array.Length - 1 - i]; // array.Length - 1 - i定位到最后一个可用的位置
array[array.Length - 1 - i] = t;
// 输出(把大值放后面)后再调整之前的元素
HeapAdjust(array, 0 , array.Length - 2 - i);
}
}
return array;
}
#endregion
}
}