排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
所需辅助空间最多:归并排序
所需辅助空间最少:堆排序
平均速度最快:快速排序
不稳定:快速排序,希尔排序,堆排序。
希尔排序时间复杂度是 O(n^(1.3-2)),空间复杂度为常数阶 O(1)。希尔排序没有时间复杂度为 O(n(logn)) 的快速排序算法快 ,因此对中等大小规模表现良好,但对规模非常大的数据排序不是最优选择,总之比一般 O(n^2 ) 复杂度的算法快得多。
时间复杂度来说:
(1)平方阶(O(n^2))排序
各类简单排序:直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlogn))排序
快速排序、堆排序和归并排序;
(3)线性阶(O(n))排序
基数排序,此外还有桶、箱排序。
当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
稳定性:
排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。
稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较;
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
选择排序算法准则:
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n.
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序 : 如果内存空间允许且要求稳定性
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
2)当n较大,内存空间允许,且要求稳定性 ,则选择归并排序
3)当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
4)一般不使用或不直接使用传统的冒泡排序。
5)基数排序
它是一种稳定的排序算法,但有一定的局限性:
1、关键字可分解。
2、记录的关键字位数较少,如果密集更好
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。
冒泡排序算法
基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
时间复杂度:(n-1)+(n-2)+...+1 = n(n-1)/2 = O(n^2)
//冒泡排序是所有排序算法中最简单、最易实现的算法,有时也称为起泡排序算法。
//用冒泡排序算法对 {14, 33, 27, 35, 10} 完成升序排序
public static void Bubble_sort(int[] list)
{
int temp = 0;
for (int i = 0; i < list.Length - 1; i++)
{
for (int j = 0; j < list.Length - i - 1; j++)
{
if (list[j] > list[j + 1])
{
temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
}
}
}
}
//调用Bubble_sort
public static void GetBubble_sort()
{
int[] list = { 14, 33, 27, 35, 10 };
Bubble_sort(list);
foreach (var item in list)
{
Console.Write(item + " ");
}
Console.WriteLine();
//10 14 27 33 35
int[] list2 = { 5, 4, 3, 1, 2 };
Bubble_sort2(list2);
foreach (var item in list2)
{
Console.Write(item + " ");
}
Console.WriteLine();
int[] list3 = { 6,4,7,5,1,3,2};
Bubble_sort3(list3);
foreach (var item in list3)
{
Console.Write(item + " ");
}
}
//算法的第一次优化
//当数组是5,4,3,1,2 的时候,实际上在第一次循环的时候整个数组就已经完成排序,
//但是常规版的算法仍然会继续后面的流程,这就是多余的了
//为了解决这个问题,我们可以设置一个标志位,用来表示当前第 i 趟是否有交换,如果有则要进行 i+1 趟,如果没有,则说明当前数组已经完成排序。
//降序
public static void Bubble_sort2(int[] list)
{
int flag = 1;
int temp = 0;
for (int i = 0; i < list.Length; i++) // 轮
{
flag = 1;
for (int j = 0; j < list.Length - i - 1; j++)
{
if (list[j] < list[j + 1])
{
temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
flag = 0;
}
}
if (flag == 1)
{
return;
}
}
}
//算法的第二次优化
//在冒泡排序中还有一个问题存在,就是第 i 趟排的第 i 小或者大的元素已经在第 i 位上了,甚至可能第 i-1 位也已经归位了,
//那么在内层循环的时候,有这种情况出现就会导致多余的比较出现。
//例如:6,4,7,5,1,3,2,当我们进行第一次排序的时候,结果为6,7,5,4,3,2,1,实际上后面有很多次交换比较都是多余的,因为没有产生交换操作。
//利用一个标志位,记录一下当前第 i 趟所交换的最后一个位置的下标,在进行第 i+1 趟的时候,只需要内循环到这个下标的位置就可以了,
//因为后面位置上的元素在上一趟中没有换位,这一次也不可能会换位置了
//降序
public static void Bubble_sort3(int[] list)
{
int flag = 1;
int temp = 0;
int index = 0; 记录最后一次交换的位置
int len = list.Length - 1;
for (int i = 0; i < list.Length - 1; i++)
{
flag = 1;
for (int j = 0; j < len; j++)
{
if (list[j] < list[j + 1])
{
temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
flag = 0;
index = j;
}
}
len = index;
if (flag == 1)
{
return;
}
}
}
插入排序算法
插入排序(InsertionSort),一般也被称为直接插入排序。
对于少量元素的排序,它是一个有效的算法。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增 1 的有序表。
在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。
插入排序的平均时间复杂度也是 O(n^2),空间复杂度为常数阶 O(1)
//插入排序,升序
public static void insertion_sort(int[] list)
{
int length = list.Length;
for(int i = 1; i< length; i++) // 从第 2 个元素(下标为 1)开始遍历
{
int insert_elem = list[i];//要插入的记录
int position = i; //记录目标元素所在的位置,从此位置向前开始遍历
//从 position 向前遍历,找到目标元素的插入位置
while (position > 0 && list[position -1] > insert_elem)
{
list[position] = list[position - 1];
position--;
}
if(position != i)
{
list[position] = insert_elem;
}
}
}
选择排序算法
基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;
然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。
对数据量较少的序列实现升序或降序排序,可以考虑使用选择排序算法,它对应的时间复杂度为O(n^2)。
选择排序算法可以看作是冒泡排序算法的“改良版”。和后者相比,选择排序算法大大减少了交换数据存储位置的操作。
//使用选择排序算法对 {14, 33, 27, 10, 35, 19, 42, 44} 实现升序排序
public static void selection_sort(int[] list)
{
int length = list.Length;
int i, j;
for(i = 0;i < length - 1; i++) // 从第 1 个元素开始遍历,直至倒数第 2 个元素(需要循环确认length-1个位置)
{
int min = i;// 事先假设最小值为第 i 个元素
for(j = i + 1; j < length; j++) // 从第 i+1 个元素开始遍历,查找真正的最小值
{
if(list[j] < list[min])
{
min = j;
}
}
// 如果最小值所在位置不为 i,交换最小值和第 i 个元素的位置
if(min != i)
{
int temp = list[min];
list[min] = list[i];
list[i] = temp;
}
}
}
希尔排序(最小增量排序)
希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。
希尔排序又称缩小增量排序,因 DL.Shell 于 1959 年提出而得名。
它通过比较相距一定间隔的元素来进行,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。
插入排序算法是通过比较元素大小和交换元素存储位置实现排序的,比较大小和移动元素的次数越多,算法的效率就越差。
希尔排序算法又叫缩小增量排序算法,是一种更高效的插入排序算法。和普通的插入排序算法相比,希尔排序算法减少了移动元素和比较元素大小的次数,从而提高了排序效率。
基本思想:算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,进行直接插入排序后,排序完成。
希尔排序算法的实现思路是:
- 将待排序序列划分成多个子序列,使用普通的插入排序算法对每个子序列进行排序;
- 按照不同的划分标准,重复执行第一步;
- 使用普通的插入排序算法对整个序列进行排序。
序列的划分方法:
待排序序列如何进行划分,划分多少次,都会影响到希尔排序算法的执行效率。
希尔排序算法没有固定的划分标准,这里给大家推荐一种常用的方法,套用如下伪代码:
输入 list //输入待排序序列 interval <- 1 // 初始值为 1 while interval < length(list) / 3: // length(list) 表示待排序序列的长度 interval = interval * 3 + 1
经过计算得出的 interval 的值,就是首次划分序列采用的标准。
后续划分整个序列,套用如下公式:
interval = (interval-1)/3
比如说计算第二次划分序列的标准,只需将第一次划分序列时计算得到的 interval 代入公式,求出的新 interval 值就是第二次采用的划分标准。
希尔排序时间复杂度是 O(n^(1.3-2)),空间复杂度为常数阶 O(1)。希尔排序没有时间复杂度为 O(n(logn)) 的快速排序算法快 ,因此对中等大小规模表现良好,但对规模非常大的数据排序不是最优选择,总之比一般 O(n^2 ) 复杂度的算法快得多。
//用希尔排序算法对 {35, 33, 42, 10, /14, 19, 27, 44} 实现升序排序
public static void shell_sort(int[] list)
{
int length = list.Length;
// 初始化间隔数为 1
int interval = 1;
// 计算最大间隔
while (interval < length / 3)
{
interval = interval * 3 + 1;
}
// 根据间隔数,不断划分序列,并对各子序列排序
//用希尔排序算法对 {35, 33, 42, 10, /14, 19, 27, 44} 实现升序排序
while (interval > 0)
{
for(int i = interval; i < length; i++)
{
int temp = list[i];
int j = i;
while(j > interval -1 && list[j-interval] >= temp)
{
list[j] = list[j - interval];
j -= interval;
}
if(j != i)
{
list[j] = temp;
}
}
interval = (interval - 1) / 3; // 计算新的间隔数,继续划分序列
}
}
归并排序算法
归并排序算法是在分治算法基础上设计出来的一种排序算法,它可以对指定序列完成升序(由小到大)或降序(由大到小)排序,对应的时间复杂度为O(nlogn),空间复杂度O(n)。
归并排序算法实现排序的思路是:
- 将整个待排序序列划分成多个不可再分的子序列,每个子序列中仅有 1 个元素;
- 所有的子序列进行两两合并,合并过程中完成排序操作,最终合并得到的新序列就是有序序列。
算法步骤:
1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4. 重复步骤3直到某一指针达到序列尾
5. 将另一序列剩下的所有元素直接复制到合并序列尾
//归并排序
//实现归并排序算法的分割操作
//merge_sort(arr, 1, 8)
public static void merge_sort(int[] arr, int p, int q)
{
// 如果数组不存在或者 [p.q] 区域不合理
if (arr == null || p >= q)
{
return;
}
int mid = (p + q) / 2;
merge_sort(arr, p, mid);
merge_sort(arr, mid + 1, q);
//对分割的 [p,mid] 和 [mid+1,q] 区域进行归并
merge(arr, p, mid, q);
}
//实现归并排序算法的归并操作
public static void merge(int[] arr, int p, int mid, int q)
{
int numL = mid - p + 1;
int numR = q - mid;
int[] leftarr = new int[numL + 1];
int[] rightarr = new int[numR + 1];
int i;
for (i = 0; i < numL; i++)
{
leftarr[i] = arr[p - 1 + i];
}
//将 leftarr 数组中最后一个元素设置为足够大的数。
leftarr[i] = 2147483647;
for ( i = 0; i < numR; i++)
{
rightarr[i] = arr[mid + i];
}
//将 rightarr 数组中最后一个元素设置为足够大的数。
rightarr[i] = 2147483647;
int j = 0;
i = 0;
//对 leftarr 和 rightarr 数组中存储的 2 个区域的元素做归并操作
for(int k = p; k <= q; k++)
{
if(leftarr[i] <= rightarr[j])
{
arr[k - 1] = leftarr[i];
i++;
}
else
{
arr[k - 1] = rightarr[j];
j++;
}
}
}
快速排序算法
提到排序算法,多数人最先想到的就是快速排序算法。快速排序算法是在分治算法基础上设计出来的一种排序算法,和其它排序算法相比,快速排序算法具有效率高、耗费资源少、容易实现等优点。
快速排序算法的实现思路是:
- 从待排序序列中任选一个元素(假设为 pivot)作为中间元素,将所有比 pivot 小的元素移动到它的左边,所有比 pivot 大的元素移动到它的右边;
- pivot 左右两边的子序列看作是两个待排序序列,各自重复执行第一步。直至所有的子序列都不可再分(仅包含 1 个元素或者不包含任何元素),整个序列就变成了一个有序序列。
真正实现快速排序算法时,我们通常会挑选待排序序列中第一个元素或者最后一个元素作为中间元素。
算法步骤:
1 从数列中挑出一个元素,称为 “基准”(pivot),
2 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
快排是最不稳定的排序算法
如:33 33’ 排序后可能变成 33’ 33
时间复杂度
平均 O(nlogn)
最差:O(N^2)
空间复杂度
递归调用消耗线空间
最优:O(logn)
最差:O(n) 退化为冒泡排序
//用快速排序算法对 {35, 33, 42, 10, 14, 19, 27, 44, 26, 31} 完成升序排序
//quick_sort(arr, 0, 9);
public static void quick_sort(int[] arr, int p, int q)
{
if (q - p <= 0)
{
return;
}
else
{
// 调用 partition() 函数,分割[p, q] 区域
int par = partition(arr, p, q);
//以 [p,par-1]作为新的待排序序列,继续分割
quick_sort(arr, p, par - 1);
//以[par+1,q]作为新的待排序序列,继续分割
quick_sort(arr, par + 1, q);
}
}
public static int partition(int[] arr, int p, int q)
{
int temp = 0;
int lo = p;
int hi = q - 1;
int pivot = arr[q];
while (true)
{
while (arr[lo] < pivot)
{
lo++;
}
while (hi > 0 && arr[hi] > pivot)
{
hi--;
}
if (lo >= hi)
{
break;
}
else
{
// 交换 arr[lo] 和 arr[hi] 的值
temp = arr[lo];
arr[lo] = arr[hi];
arr[hi] = temp;
// lo 和 hi 都向前移动一个位置,准备继续遍历
lo++;
hi--;
}
}
// 交换 arr[lo] 和 arr[q] 的值
temp = arr[lo];
arr[lo] = pivot;
arr[q] = temp;
// 返回中间值所在序列中的位置
return lo;
}
计数排序算法
通过统计序列中各个元素出现的次数,完成对整个序列的升序或降序排序,这样的排序算法称为计数排序算法。
算法步骤:
第一步:找出原数组中元素值最大的,记为max。
第二步:创建一个新数组count,其长度是max加1,其元素默认值都为0。
第三步:遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。
第四步:创建结果数组result,起始索引index。
第五步:遍历count数组,找出其中元素值大于0的元素,将其对应的索引作为元素值填充到result数组中去,每处理一次,count中的该元素值减1,直到该元素值不大于0,依次处理count中剩下的元素。
第六步:返回结果数组result。
//采用计数排序算法对 {4, 2, 2, 8, 3, 3, 1} 进行升序排序
public static void countingSort(int[] list)
{
int length = list.Length;
//第 1 步,找到序列中的最大值
int max = GetMax(list);
//第 2 步,初始化一个 array[max+1]
int[] array = new int[max + 1];
int[] output = new int[length];
//第 3 步,统计各个元素的出现次数,并存储在相应的位置上
for (int i = 0; i < length; i++)
{
array[list[i]]++;
}
// 第 4 步,累加 array 数组中的出现次数
for (int i = 1; i <= max; i++)
{
array[i] += array[i - 1];
}
// 第 5 步,根据 array 数组中的信息,找到各个元素排序后所在位置,存储在 output 数组中
for (int i = length - 1; i >= 0; i--)
{
output[array[list[i]] - 1] = list[i];
array[list[i]]--;
}
// 将 output 数组中的数据原封不动地拷贝到 list 数组中
for (int i = 0; i < length; i++)
{
list[i] = output[i];
}
}
找到数组中的最大值
public static int GetMax(int[] list)
{
int max = list[0];
for (int i = 1; i < list.Length; i++)
{
if (list[i] > max)
{
max = list[i];
}
}
return max;
}
基数排序算法
基数排序算法适用于对多个整数或者多个字符串进行升序或降序排序
基数排序算法的实现思路是:对于待排序序列中的各个元素,依次比较它们包含的各个数字或字符,根据比较结果调整各个元素的位置,最终就可以得到一个有序序列。
对于待排序的整数序列,依次比较各个整数的个位数、十位数、百位数......,数位不够的用 0 表示;对于待排序的字符串序列,依次比较各个字符串的第一个字符、第二个字符、第三个字符......,位数不够的用 NULL 表示。
//采用基数排序算法对 {121, 432, 564, 23, 1, 45, 788} 进行升序排序
public static void radixSort(int[] array)
{
int max = GetMax(array); //找到序列中的最大值
// 根据最大值具有的位数,从低位依次调用计数排序算法
for (int place = 1; max / place > 0; place *= 10)
{
countingSort(array, place);
}
}
// 计数排序算法,place 表示以指定数位为准,对序列中的元素进行排序
public static void countingSort(int[] array,int place)
{
int size = array.Length;
int[] output = new int[size];
// 假设第一个元素指定数位上的值最大
int max = (array[0] / place) % 10;
// 找到真正数位上值最大的元素
for (int i = 1; i < size; i++)
{
if (((array[i] / place) % 10) > max)
max = array[i];
}
// 创建并初始化 count 数组
int[] count = new int[max + 1];
for (int i = 0; i < max; ++i)
count[i] = 0;
// 统计各个元素出现的次数
for (int i = 0; i < size; i++)
count[(array[i] / place) % 10]++;
// 累加 count 数组中的出现次数
for (int i = 1; i < 10; i++)
count[i] += count[i - 1];
// 根据 count 数组中的信息,找到各个元素排序后所在位置,存储在 output 数组中
for (int i = size - 1; i >= 0; i--)
{
output[count[(array[i] / place) % 10] - 1] = array[i];
count[(array[i] / place) % 10]--;
}
// 将 output 数组中的数据原封不动地拷贝到 array 数组中
for (int i = 0; i < size; i++)
array[i] = output[i];
}
桶排序算法
桶排序(又称箱排序)是一种基于分治思想、效率很高的排序算法,理想情况下对应的时间复杂度为 O(n)。
桶排序算法的实现思路是:将待排序序列中的元素根据规则分组,每一组采用快排、插入排序等算法进行排序,然后再按照次序将所有元素合并,就可以得到一个有序序列。
堆排序算法
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。