算法质量的衡量标准:
1:时间复杂度:分析关键字比较次数和记录的移动次数;
2:空间复杂度:需要的辅助内存;
3:稳定性:相同的关键字计算后,次序是否不变。
排序的分类
1:选择排序(直接选择排序、堆排序)
2:交换排序(冒泡排序、快速排序)
3:插入排序(直接插入排序、折半插入排序、Shell排序)
4:归并排序
5:捅式排序
6:基数排序
1:直接选择排序
思路:
第一次从R[0]~R[n-1]中选取最小值,与R[0]交换,第二次从R{1}~R[n-1]中选取最小值,与R[1]交换,...., 第i次从R[i-1]~R[n-1]中选取最小值,与R[i-1]交换,.....,第n-1次从R[n-2]~R[n-1]中选取最小值,与R[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。
效率分析:
在直接选择排序中,共需要进行n-1次选择和交换,每次选择需要进行 n-i 次比较 (1<=i<=n-1),而每次交换最多需要3次移动,因此,总的比较次数C=1/2(n*n - n),
总的移动次数 3(n-1).由此可知,直接选择排序的时间复杂度为 O(n2) (n的平方),所以当记录占用字节数较多时,通常比直接插入排序的执行速度快些。
由于在直接选择排序中存在着不相邻元素之间的互换,因此,直接选择排序是一种不稳定的排序方法。
代码:
private static void SelectSort(int[] data)
{
int arrayLength = data.Length;
//依次进行n-1次比较,第i次比较将第i大的值选出放在i的位置上
for (int i = 0; i < arrayLength - 1; i++)
{
//保留最小值的索引
int minIndex = i;
//将第i个数据和它后面的数据比较
for (int j = i + 1; j < arrayLength; j++)
{
if (data[minIndex] > data[j])
{
minIndex = j;
}
}
//交换数据
if (minIndex != i)
{
int temp = data[i];
data[i] = data[minIndex];
data[minIndex] = temp;
}
}
}
2:堆排序
思路:
堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。
用大根堆排序的基本思想
① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
② 再将关键字最大的记录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]调整为堆。
……
直到无序区只有一个元素为止。
效率分析:
排序的时间,主要由建立初始]堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。
堆排序的最坏时间复杂度为O(nlogn)。堆序的平均性能较接近于最坏性能。
由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
堆排序是就地排序,辅助空间为O(1),
它是不稳定的排序方法。
代码:
private static void HeapSort(int[] data)
{
int arrayLength = data.Length;
//建堆
for (int i = 0; i < arrayLength - 1; i++)
{
//建堆
BuildMaxdHeap(data, arrayLength - 1 - i);
//交换堆顶和最后一个元素
Swap(data, 0, arrayLength - 1 - i);
}
}
private static void BuildMaxdHeap(int[] data, int lastIndex)
{
//从最后一个节点的父节点开始
for (int i = (lastIndex - 1) / 2; i >= 0; i--)
{
//当前位置
int k = i;
//如果当前k节点存在子节点
while (k * 2 + 1 <= lastIndex)
{
int biggerIndex = k * 2 + 1;
//判断右子节点是否存在
if (biggerIndex < lastIndex)
{
if (data[biggerIndex] < data[biggerIndex + 1])
biggerIndex++;
}
if (data[k] < data[biggerIndex])
{
Swap(data, k, biggerIndex);
k = biggerIndex;
}
else
{
break;
}
}
}
}
private static void Swap(int[] data, int i, int j)
{
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
3:冒泡排序
思路:
次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。至此第一趟结束,将最大的数放到了最后。在第二趟:仍从第一对数开始比较(因为可能由于第2个数和第3个数的交换,使得第1个数不再小于第2个数),将小数放前,大数放后,一直比较到倒数第二个数(倒数第一的位置上已经是最大的),第二趟结束,在倒数第二的位置上得到一个新的最大数(其实在整个数列中是第二大的数)。如此下去,重复以上过程,直至最终完成排序。
效率分析:
若记录序列的初始状态为"正序",则冒泡排序过程只需进行一趟排序,在排序过程中只需进行n-1次比较,且不移动记录;反之,若记录序列的初始状态为"逆序",则需进行n(n-1)/2次比较和记录移动。因此冒泡排序总的时间复杂度为O(n*n)。
代码:
private static void BubbleSort(int[] data)
{
int arrayLength = data.Length;
for (int i = 0; i < arrayLength - 1; i++)
{
bool flag = false;
for (int j = 0; j < arrayLength - 1 - i; j++)
{
if (data[j] > data[j + 1])
{
int temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
flag = true;
}
}
if (!flag)
{
break;
}
}
}
private static void BubbleSort(int[] data)
{
int arrayLength = data.Length;
for (int i = 0; i < arrayLength - 1; i++)
{
bool flag = false;
for (int j = 0; j < arrayLength - 1 - i; j++)
{
if (data[j] > data[j + 1])
{
int temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
flag = true;
}
}
if (!flag)
{
break;
}
}
}
4:快速排序
思路:
1)设置两个变量I、J,排序开始的时候:I=0,J=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即 key=A[0];
3)从J开始向前搜索,即由后开始向前搜索(J=J-1即J--),找到第一个小于key的值A[j],A[j]与A[i]交换;
4)从I开始向后搜索,即由前开始向后搜索(I=I+1即I++),找到第一个大于key的A[i],A[i]与A[j]交换;
5)重复第3、4、5步,直到 I=J; (3,4步是在程序中没找到时候j=j-1,i=i+1,直至找到为止。找到并交换的时候i, j指针位置不变。另外当i=j这过程一定正好是i+或j-完成的最后令循环结束。)
效率分析:
冒泡排序的一种改进
代码:
private static void QuickSort(int[] data)
{
//调用子排序
SubSort(data, 0, data.Length - 1);
}
//对data数组中start到end范围内的子序列进行排序
private static void SubSort(int[] data, int start, int end)
{
if (start < end)
{
//第一个元素作为分界值
int middle = data[start];
int i = start;
int j = end + 1;
while (true)
{
while (i < end && data[++i] <= middle) ;
while (j > start && data[--j] >= middle) ;
if (i < j)
{
Swap(data, i, j);
}
else
{
break;
}
}
Swap(data, start, j);
//递归进行左子排序
SubSort(data, start, j - 1);
//递归进行右子排序
SubSort(data, j + 1, end);
}
}
5:直接插入排序
思路:
每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。
第一趟比较前两个数,然后把第二个数按大小插入到有序表中; 第二趟把第三个数据与前两个数从前向后扫描,把第三个数按大小插入到有序表中;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。
直接插入排序属于稳定的排序,最坏时间复杂性为Θ(n^2),空间复杂度为O(1)。
直接插入排序是由两层嵌套循环组成的。外层循环标识并决定待比较的数值。内层循环为待比较数值确定其最终位置。直接插入排序是将待比较的数值与它的前一个数值进行比较,所以外层循环是从第二个数值开始的。当前一数值比待比较数值大的情况下继续循环比较,直到找到比待比较数值小的并将待比较数值置入其后一位置,结束该次循环
效率分析:
速度慢,空间效率高,稳定。
代码:
private static void InsertSort(int[] data)
{
for (int i = 1; i < data.Length; i++)
{
//备份data[i]的值
int temp = data[i];
if (data[i] < data[i - 1])
{
int j = i - 1;
//通过比较找出要插入的位置,同时将大的数值向右移动
for (; j >= 0 && data[j] > temp; j--)
{
data[j + 1] = data[j];
}
//将temp插入合适的位置
data[j + 1] = temp;
}
}
}
6:折半插入排序
思路:
折半插入排序(binary insertion sort)是对插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。
折半插入排序算法的具体操作为:在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为a[high],则轮比较时将待插入元素与a[m],其中m=(low+high)/2相比较,如果比参考元素小,则选择a[low]到a[m-1]为新的插入区域(即high=m-1),否则选择a[m+1]到a[high]为新的插入区域(即low=m+1),如此直至low<=high不成立,即将此位置之后所有元素后移一位,并将新元素插入a[high+1]。
效率分析:
折半插入排序算法是一种稳定的排序算法,比直接插入算法明显减少了关键字之间比较的次数,因此速度比直接插入排序算法快,但记录移动的次数没有变,所以折半插入排序算法的时间复杂度仍然为O(n^2),与直接插入排序算法相同。
代码:
private static void BinaryInsertSort(int[] data)
{
for (int i = 1; i < data.Length; i++)
{
//备份data[i]的值
int temp = data[i];
int low = 0;
int high = i - 1;
while (low <= high)
{
//取low到high中间的索引
int middle = (low + high) / 2;
//确定temp在中间值的哪侧
if (temp > data[middle])
{
low = middle + 1;
}
else
{
high = middle - 1;
}
}
//将low到i处的所有元素向后整体移一位
for (int j = i; j > low; j--)
{
data[j] = data[j - 1];
}
//将temp插入合适的位置
data[low] = temp;
}
}
7:Shell排序
思路:
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<;…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
效率分析:
性能由于直接插入排序,不稳定
代码:
private static void ShellSort(int[] data)
{
int arrayLength = data.Length;
//可增变量
int h = 1;
while (h <= arrayLength / 3)
{
h = h * 3 + 1;
}
while (h > 0)
{
for (int i = h; i < arrayLength; i++)
{
//备份当前值
int temp = data[i];
if (data[i] < data[i - h])
{
int j = i - h;
//整体向后移动h格
for (; j >= 0 && data[j] > temp; j -= h)
{
data[j + h] = data[j];
}
data[j + h] = temp;
}
}
h = (h - 1) / 3;
}
}
private static void ShellSort(int[] data)
{
int arrayLength = data.Length;
//可增变量
int h = 1;
while (h <= arrayLength / 3)
{
h = h * 3 + 1;
}
while (h > 0)
{
for (int i = h; i < arrayLength; i++)
{
//备份当前值
int temp = data[i];
if (data[i] < data[i - h])
{
int j = i - h;
//整体向后移动h格
for (; j >= 0 && data[j] > temp; j -= h)
{
data[j + h] = data[j];
}
data[j + h] = temp;
}
}
h = (h - 1) / 3;
}
}
8:归并排序
思路:
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
设定两个指针,最初位置分别为两个已经排序序列的起始位置
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针达到序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
效率分析:
速度仅次于快速排序,但较稳定
代码:
private static void MergeSort(int[] data)
{
Sort(data, 0, data.Length - 1);
}
//将left到right范围内的数组进行归并排序
private static void Sort(int[] data, int left, int right)
{
if (left < right)
{
int center = (left + right) / 2;
//对左侧数组递归排序
Sort(data, left, center);
//对右侧数组递归排序
Sort(data, center + 1, right);
//合并
Merge(data, left, center, right);
}
}
//将两个数组进行归并
private static void Merge(int[] data, int left, int center, int right)
{
int[] tempArray = new int[data.Length];
int middle = center + 1;
int third = left;
int temp = left;
while (left <= center && middle <= right)
{
//从两个数组中取出小的放入中间的数组
if (data[left] <= data[middle])
{
tempArray[third++] = data[left++];
}
else
{
tempArray[third++] = data[middle++];
}
}
//将余下的依次放入中间数组
while (middle <= right)
{
tempArray[third++] = data[middle++];
}
while (left <= center)
{
tempArray[third++] = data[left++];
}
//将中间数组中的内容复制到原数组
while (temp <= right)
{
data[temp] = tempArray[temp++];
}
}
private static void MergeSort(int[] data)
{
Sort(data, 0, data.Length - 1);
}
//将left到right范围内的数组进行归并排序
private static void Sort(int[] data, int left, int right)
{
if (left < right)
{
int center = (left + right) / 2;
//对左侧数组递归排序
Sort(data, left, center);
//对右侧数组递归排序
Sort(data, center + 1, right);
//合并
Merge(data, left, center, right);
}
}
//将两个数组进行归并
private static void Merge(int[] data, int left, int center, int right)
{
int[] tempArray = new int[data.Length];
int middle = center + 1;
int third = left;
int temp = left;
while (left <= center && middle <= right)
{
//从两个数组中取出小的放入中间的数组
if (data[left] <= data[middle])
{
tempArray[third++] = data[left++];
}
else
{
tempArray[third++] = data[middle++];
}
}
//将余下的依次放入中间数组
while (middle <= right)
{
tempArray[third++] = data[middle++];
}
while (left <= center)
{
tempArray[third++] = data[left++];
}
//将中间数组中的内容复制到原数组
while (temp <= right)
{
data[temp] = tempArray[temp++];
}
}
9:捅式排序
思路:
效率分析:
代码:
private static void BucketSort(int[] data, int min, int max)
{
int arrayLength = data.Length;
int[] temp = new int[arrayLength];
//记录待排序元素的信息
int[] buckets = new int[max - min];
//计算每个元素在序列中出现的次数
for (int i = 0; i < arrayLength; i++)
{
buckets[data[i] - min]++;
}
//计算“落入”各捅内的元素在有序序列中的位置
for (int i = 1; i < max - min; i++)
{
buckets[i] = buckets[i] + buckets[i - 1];
}
//将当前数组备份到temp中
data.CopyTo(temp, 0);
//根据buckets数组中的信息将待排序的各元素让入相应的位置
for (int k = arrayLength - 1; k >= 0; k--)
{
data[--buckets[temp[k] - min]] = temp[k];
}
}
private static void BucketSort(int[] data, int min, int max)
{
int arrayLength = data.Length;
int[] temp = new int[arrayLength];
//记录待排序元素的信息
int[] buckets = new int[max - min];
//计算每个元素在序列中出现的次数
for (int i = 0; i < arrayLength; i++)
{
buckets[data[i] - min]++;
}
//计算“落入”各捅内的元素在有序序列中的位置
for (int i = 1; i < max - min; i++)
{
buckets[i] = buckets[i] + buckets[i - 1];
}
//将当前数组备份到temp中
data.CopyTo(temp, 0);
//根据buckets数组中的信息将待排序的各元素让入相应的位置
for (int k = arrayLength - 1; k >= 0; k--)
{
data[--buckets[temp[k] - min]] = temp[k];
}
}
10:基数排序
思路:
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
效率分析:
时间效率:设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(n),共进行d趟分配和收集。 空间效率:需要2*radix个指向队列的辅助空间,以及用于静态链表的n个指针。
代码:
/*
* radix 指定关键字拆分的进制,如10为十进制
* d 指定关键字拆分成几个子关键字
*/
private static void RadixSort(int[] data, int radix, int d)
{
int arrayLength = data.Length;
int[] temp = new int[arrayLength];
int[] buckets = new int[radix];
//依次从最高位的子关键字对待排数据进行排序
for (int i = 0, rate = 1; i < d; i++)
{
//重置临时数组
Array.Clear(buckets, 0, arrayLength);
//将data数组进行备份
data.CopyTo(temp, 0);
//计算每个待排序数据的子关键字
for (int j = 0; j < arrayLength; j++)
{
int subKey = (temp[j] / rate) % radix;
buckets[subKey]++;
}
for (int j = 1; j < radix; j++)
{
buckets[j] = buckets[j] + buckets[j - 1];
}
//按照子关键字对指定数据进行排序
for (int m = arrayLength - 1; m >= 0; m--)
{
int subKey = (temp[m] / rate) % radix;
data[--buckets[subKey]] = temp[m];
}
rate *= radix;
}
}