排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。分内部排序和外部排序。若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。内部排序的过程是一个逐步扩大记录的有序序列长度的过程。
将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
稳定排序:假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字,在用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变,则这种排序方法是稳定的。其中冒泡,插入,基数,归并属于稳定排序,选择,快速,希尔,堆属于不稳定排序。
就地排序:若排序算法所需的辅助空间并不依赖于问题的规模n,即辅助空间为O(1),则称为就地排序。
排序分为以下四类共七种排序方法:
交换排序:1) 冒泡排序
2) 快速排序
选择排序:3) 直接选择排序
4) 堆排序
插入排序:5) 直接插入排序
6) 希尔排序
合并排序:7) 合并排序
一、冒泡排序:
冒泡排序是一种简单的排序方法,其基本思想是:通过相邻元素之间的比较和交换,使关键字较小的元素逐渐从底部移向顶部,就像水底下的气泡一样逐渐向上冒泡,所以使用该方法的排序称为“冒泡”排序。
优点:稳定
缺点:慢,每次只移动相邻的两个元素。
时间复杂度:理想情况下(数组本来就是有序的),此时最好的时间复杂度为o(n),最坏的时间复杂度(数据反序的),此时的时间复杂度为o(n*n) 。冒泡排序的平均时间负责度为o(n*n).
/// <summary>
/// 冒泡排序
/// </summary>
/// <param name="arry">要排序的整数数组</param>
public static void BubbleSort(this int[] arry)
{
for (int i = 0; i < arry.Length; i++)
{
for (int j = 0; j < arry.Length - 1 - i; j++)
{
//比较相邻的两个元素,如果前面的比后面的大,则交换位置
if (arry[j] > arry[j + 1])
{
int temp = arry[j + 1];
arry[j + 1] = arry[j];
arry[j] = temp;
}
}
}
}
二、快速排序:
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
写法一:
public static void QuickSort(this int[] arr,int left,int right)
{
int i = left;
int j = right;
if (i > j) return;
int pivot = i;//设置基准值,将最左端元素作为基准值
while (i != j)
{
while (i < j && arr[j] >=arr[i]) j--;//右侧扫描
if (i < j) {
ChangeValue(arr, i, j);// //将较小记录交换到前面
i++;
}
while (i < j && arr[j] >= arr[i]) i++;//左侧扫描
if (i < j)
{
ChangeValue(arr, i, j);//将较大记录交换到后面
j--;
}
}
pivot=i;
QuickSort(arr, left, pivot-1);//递归地对左侧子序列进行快速排序
QuickSort(arr, pivot + 1,right);//递归地对右侧子序列进行快速排序
}
private static void ChangeValue(int[] arr, int leftIndex, int rightIndex)
{
if (arr[leftIndex] > arr[rightIndex])
{
int temp = arr[leftIndex];
arr[leftIndex] = arr[rightIndex];
arr[rightIndex] = temp;
}
}
写法二:
/// <summary>
/// 快速排序算法
/// </summary>
/// <param name="list"></param>
/// <param name="low"></param>
/// <param name="high"></param>
public static void QuickSort(List<int> list, int low, int high)
{
if (low < high)
{
//分割数组,找到枢轴
int pivot = Partition(list,low,high);
//递归调用,对低子表进行排序
QuickSort(list,low,pivot-1);
//对高子表进行排序
QuickSort(list,pivot+1,high);
}
}
/// <summary>
/// 分割列表,找到枢轴
/// </summary>
/// <param name="list"></param>
/// <param name="low"></param>
/// <param name="high"></param>
/// <returns></returns>
private static int Partition(List<int> list, int low, int high)
{
//用列表的第一个记录作枢轴记录
int pivotKey = list[low];
while (low < high)
{
while (low < high && list[high] >= pivotKey)
high--;
Swap(list,low,high);//交换
while (low < high && list[low] <= pivotKey)
low++;
Swap(list,low,high);
}
//返回枢轴所在位置
return low;
}
/// <summary>
/// 交换列表中两个位置的元素
/// </summary>
/// <param name="list"></param>
/// <param name="low"></param>
/// <param name="high"></param>
/// <returns></returns>
private static void Swap(List<int> list, int low, int high)
{
int temp = -1;
if (list != null && list.Count > 0)
{
temp = list[low];
list[low] = list[high];
list[high] = temp;
}
}
三、直接选择排序:
设所排序序列的记录个数为n。i取1,2,…,n-1,从所有n-i+1个记录(Ri,Ri+1,…,Rn)中找出排序码最小的记录,与第i个记录交换。执行n-1趟 后就完成了记录序列的排序。
在简单选择排序过程中,所需移动记录的次数比较少。最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。
最坏情况下,即待排序记录初始状态是按逆序排列的,则需要移动记录的次数最多为3(n-1)。简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况无关。当i=1时,需进行n-1次比较;当i=2时,需进行n-2次比较;依次类推,共需要进行的比较次数是(n-1)+(n-2)+…+2+1=n(n-1)/2,即进行比较操作的时间复杂度为O(n^2),进行移动操作的时间复杂度为O(n)。
public static void SelectSort(this int[] arr)
{
for (int i = 0; i < arr.Length; i++)
{
for (int j = i+1; j < arr.Length; j++)
{
if (arr[i] > arr[j])
{
ChangeValue(arr, i, j);
}
}
}
}
四、堆排序:
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。
这里的堆(二叉堆),指得不是堆栈的那个堆,而是一种数据结构。堆是具有下列性质的完全二叉树:每个结点的值都小于或等于其左右孩子结点的值(小根堆);或者每个结点的值都大于或等于其左右孩子结点的值(大根堆)。
1.建堆:将待排序的序列构造成一个大根堆:
/// <summary>
///调整某个元素使它到到满足大根堆的位置
/// </summary>
/// <param name="arr"></param>
/// <param name="parent">当前节点(父节点)</param>
/// <param name="length"></param>
public static void HeapAdjust(int[] arr, int parent, int length)
{
int temp = arr[parent];
int child = 2 * parent + 1;//左孩子结点索引为父节点的2倍加1(根节点索引从0开始)
while (child < length)
{
if (child + 1 < length && arr[child] < arr[child + 1]) child++;//比较左右节点值的大小,child指向最大的节点
if (temp > arr[child])//父节点大于子节点,则满足大根堆定义
break;
else
{
arr[parent] = arr[child];//父节点小于子节点,则将子节点赋值给父节点
arr[child] = temp;
parent = child;
child = 2 * parent + 1;
}
}
arr[parent] = temp;
}
/// <summary>
/// 建立大根堆:由下(非叶子节点)到上
/// </summary>
/// <param name="arr"></param>
public static void BuildHeap(int[] arr)
{
for (int i = arr.Length / 2 - 1; i > = 0; i--)//arr.Lenght/2-1为最下面的那个非叶子节点的下标
{
HeapAdjust(arr, i, arr.Length);
}
}
2.排序:
将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key
由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。
……
直到无序区只有一个元素为止。
public static void HeapSort(int[] arr)
{
BuildHeap(arr);//建立大根堆
for (int i = arr.Length-1; i >=0; i--)
{
ChangeValue(arr, 0, i);//将堆顶元素和最后一个元素交换
HeapAdjust(arr, 0, i);//将剩余的堆继续调整为大根堆
}
}
五、直接插入排序:
每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。
第一趟比较前两个数,然后把第二个数按大小插入到有序表中; 第二趟把第三个数据与前两个数从前向后扫描,把第三个数按大小插入到有序表中;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。
直接插入排序属于稳定的排序,最坏时间复杂性为O(n^2),空间复杂度为O(1)。
直接插入排序是由两层嵌套循环组成的。外层循环标识并决定待比较的数值。内层循环为待比较数值确定其最终位置。直接插入排序是将待比较的数值与它的前一个数值进行比较,所以外层循环是从第二个数值开始的。当前一数值比待比较数值大的情况下继续循环比较,直到找到比待比较数值小的并将待比较数值置入其后一位置,结束该次循环。
值得注意的是,我们必需用一个存储空间来保存当前待比较的数值,因为当一趟比较完成时,我们要将待比较数值置入比它小的数值的后一位 插入排序类似玩牌时整理手中纸牌的过程。
插入排序的基本方法是:每步将一个待排序的记录按其关键字的大小插到前面已经排序的序列中的适当位置,直到全部记录插入完毕为止。
public static void InserSort(int[] arr)
{
//从无序序列中取出第一条记录(注意无序区是从第二个元素开始的,第一个元素作为哨兵元素)
for (int i = 1; i < arr.Length; i++)
{
if (arr[i] < arr[i - 1])
{
int temp = arr[i];
int j;
//遍历有序序列
//如果有序序列中最末元素比临时元素(无序序列第一个元素)大,则将有序序列中比临时元素大的元素依次后移
for (j = i - 1; j >= 0 && temp < arr[j]; j--)
{
arr[j + 1] = arr[j];
}
//将临时元素插入到腾出的位置中(为比有序序列中当前遍历到的元素arr[j]大,比当前遍历到的元素后一个元素arr[j+1]小,arr[j+1]已经腾出来了)
arr[j + 1] = temp;
}
}
}
六、希尔排序:
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。
插入排序是将当前待排序的元素与前面所有的元素比较,而希尔排序是将当前元素与前面增量位置上的元素进行比较,然后,再将该元素插入到合适位置。当一趟希尔排序完成后,处于增量位置上的元素是有序的。
希尔排序算法的效率依赖于增量的选取:
假设增量序列为 h(1),h(2).....h(k),其中h(1)必须为1,且h(1)<h(2)<...h(k) 。
第一趟排序时在增量为h(k)的各个元素上进行比较;
第二趟排序在增量为h(k-1)的各个元素上进行比较;
..........
最后一趟在增量h(1)上进行比较。由此可以看出,每进行一趟排序,增量是一个不断减少的过程,因此称之为缩小增量。
当增量减少到h(1)=1时,这里完全就是插入排序了,而在此时,整个元素经过前面的 k-1 趟排序后,已经变得基本有序了,而我们知道,对于插入排序而言,当待排序的数组 基本有序时,插入排序的效率是非常高的。因此,希尔排序就是利用“增量”技巧将插入排序的平均时间复杂度O(N^2)降低为O(n*Logn)
假设原始数组为: 81 94 11 96 12 35 增量序列为 h(1)=1 h(2)=3
这里一共有两个增量序列,故一共有两趟排序。第一趟按照增量3来排序
处于增量3上的元素集合如下:<81 96>,<94 12>,<11 35>排序之后变成:<81 96>,<12 94>,<11 35>
即,数组变成了:81 12 11 96 94 35
可以看出,在增量为3的各个位置上的元素都是有序的。
经过前面一趟排序,此时数组已经基本有序,再使用增量为1的排序时(插入排序),比较的次数将会大大地降低。
第二趟按增量为h(1)=1 来排序,排序后数组变成:
11 12 35 81 94 96
public static void ShellSort(int[] arr)
{
int range = arr.Length / 2;//增量取数组长度的一半
while (range >= 1)
{
//无序序列
for (int i = range; i < arr.Length; i++)
{
int temp = arr[i];
int j;
//对每个增量序列单独进行插入排序
for ( j =i-range; j >=0&& arr[j]>temp; j=j-range)
{
arr[j + range] = arr[j];
}
arr[j + range] = temp;
}
//缩小增量
range = range / 2;
}
}
七、归并排序:
原理:
把原始数组分成若干子数组,对每一个子数组进行排序,
继续把子数组与子数组合并,合并后仍然有序,直到全部合并完,形成有序的数组
举例:
无序数组[6 2 4 1 5 9]
先看一下每个步骤下的状态,完了再看合并细节
第一步 [6 2 4 1 5 9]原始状态
第二步 [2 6] [1 4] [5 9]两两合并排序,排序细节后边介绍
第三步 [1 2 4 6] [5 9]继续两组两组合并
第四步 [1 2 4 5 6 9]合并完毕,排序完毕
输出结果[1 2 4 5 6 9]
合并细节:
详细介绍第二步到第三步的过程,其余类似
第二步:[2 6] [1 4] [5 9]
两两合并,其实仅合并[2 6] [1 4],所以[5 9]不管它,
原始状态
第一个数组[2 6]
第二个数组[1 4]
--------------------
第三个数组[...]
第1步,顺序从第一,第二个数组里取出一个数字:2和1
比较大小后将小的放入第三个数组,此时变成下边这样
第一个数组[2 6]
第二个数组[4]
--------------------
第三个数组[1]
第2步,继续刚才的步骤,顺序从第一,第二个数组里取数据,2和4,
同样的比较大小后将小的放入第三个数组,此时状态如下
第一个数组[6]
第二个数组[4]
--------------------
第三个数组[1 2]
第3步,再重复前边的步骤变成,将较小的4放入第三个数组后变成如下状态
第一个数组[6]
第二个数组[...]
--------------------
第三个数组[1 2 4]
第4步,最后将6放入,排序完毕
第一个数组[...]
第二个数组[...]
--------------------
第三个数组[1 2 4 6]
[ 1 2 4 6 ]与[ 5 9 ]的合并过程与上边一样 ,不再分解。
/// <summary>
/// 将2个有序数列a[first...mid-1]和a[mid...last]合并成有序序列
/// </summary>
/// <param name="arr"></param>
/// <param name="first"></param>
/// <param name="mid"></param>
/// <param name="last"></param>
/// <param name="tempArr"></param>
public static void Merge(int[] arr, int first, int mid, int last, int[] tempArr)
{
int i = first, j = mid;
int m = mid, n = last;
int k = 0;
while (i < m && j < n)
{
if (arr[i] < arr[j])
tempArr[k++] = arr[i++];
else
tempArr[k++] = arr[j++];
}
while (i < m)
tempArr[k++] = arr[i++];
while (j < n)
tempArr[k++] = arr[j++];
//将排好序的数组重新赋值给原始数组
for (i = 0; i < k; i++)
{
arr[first + i] = tempArr[i];
}
}
public static void MergeSort(int[] arr, int first, int last, int[] tempArr)
{
if (first + 1< last)
{
int mid = (first + last) / 2;
Console.WriteLine("{0}-{1}-{2}", first, mid, last);
MergeSort(arr, first, mid, tempArr);//左边分割
MergeSort(arr, mid, last, tempArr);//右边分割
Merge(arr, first, mid, last, tempArr);
}
}
main:
int[] arr = { 4,62,1,45,34,21,5}; //{ 3,4,6,9,10,23,44 };
int[] arr1 = new int[arr.Length];
SortBLL.MergeSort(arr,0,arr.Length,arr1);