常用排序算法分析
- 常用排序算法有冒泡排序,选择排序,插入排序,堆排,希尔排序和快排;
- 本文尽可能详尽地分析每一种排序算法的定义,实现代码,算法时间复杂度和稳定性。
1.冒泡排序(BubbleSort)
- 定义:冒泡排序,是模拟气泡上升过程的排序。气泡在上升过程体积不断变大,相似与排序比较中,相邻的两个“气泡”比较大小,大的“气泡”换到后面,一次排序后最大的“气泡”已经到达最后面,这样下次排序就可以不用再处理,只处理剩余的“气泡”,重复N-1次排序(要排序的数量N),完成排序。
-
实现算法(C#):
static void BubbleSort(int[] array)
{
int n = array.Length; //数组的长度
for (int i = 0; i < n - 1; i++)
for (int j = 0; j < n - 1 - i; j++)
if (array[j] > array[j + 1])
{
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
- 时间复杂度:第一个for循环执行了n次,第二个for循环每次执行(n-1-i)次,一共n次,总共是 n*(n-1-i)次,根据时间复杂度算法,取最高次幂去系数,O(n2)
- 稳定性:由于算法中比较大小冒泡排序就是把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
2.选择排序(SelectionSort)
- 定义:首先在未排序序列中找到最小元素,记录索引值,偏离完毕,将索引值所指元素与第一个元素交换,然后,再从剩余未排序元素中继续寻找最小元素,记录索引值,取索引值所指元素与第二个元素交换。以此类推,直到所有元素均排序完毕。
第2趟:12 20 80 91 56
第3趟:12 20 56 91 80
第4趟:12 20 56 80 91
- 实现算法(C#):
-
static void SelectionSort(int[] array) { int n = array.Length; //数组的长度 int min_index ; //记录遍历数组时最小值的下标 for (int i = 0; i < n - 1; i++) { min_index = i; for (int j = i + 1; j <= n - 1; j++) { if (array[j] < array[min_index]) min_index = j; } if (i != min_index) //说明存在比i所指元素更小的元素,否则i==min_index { int temp = array[i]; array[i] = array[min_index]; array[min_index] = temp; } } }
- 时间复杂度:两个for循环,第一个for循环执行了n次,第二个for循环,每次执行n-1-i次,循环n次,一共进行了(n-1+n-2+...+1)次,根据 时间复杂度算法,取最高次幂去系数,O(n2).
- 稳定性:假设遇到相同元素,由于在前面的元素先被获取,比较过程中相同元素是不会交换的。所以相同元素的前后顺序并没有改变,所以选择排序是一种稳定排序算法.
3.插入排序(Insert)
- 定义:插入即表示将一个新的数据插入到一个有序数组中,并继续保持有序。例如有一个长度为N的无序数组,进行N-1次的插入即能完成排序;第一次,数组第1个数认为是有序的数组,将数组第二个元素插入仅有1个有序的数组中;第二次,数组前两个元素组成有序的数组,将数组第三个元素插入由两个元素构成的有序数组中......第N-1次,数组前N-1个元素组成有序的数组,将数组的第N个元素插入由N-1个元素构成的有序数组中,则完成了整个插入排序
- 实现代码(C#):
static void InsertSort(int[] array) { int n = array.Length; //数组长度 for (int i = 1; i < n; i++) { int insert_num = array[i]; int j; for (j = i; j > 0 && array[j-1] > insert_num ; j--) { array[j] = array[j-1]; //从排序好的序列后面开始比较,将比插入元素大的元素向后挪 } //循环最后一次赋值剩余array[j-1],经过自减后 j 就是最后一个未赋值元素的坐标 array[j] = insert_num; } }
- 时间复杂度:如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n2)
- 稳定性:插入元素是从原先有序的序列后面开始比较,当遇到相同的元素时,也只会把新元素插入到相同元素的后面,不会影响两者的先后顺序,所以插入排序是稳定的算法。
4.希尔排序(ShellSort)
- 定义:希尔排序是插入排序的一种。是针对插入排序算法的改进。该方法又称缩小增量排序。先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1,即所有记录放在同一组中进行直接插入排序为止。
- 例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:
-
13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10
然后我们对每列(每一竖,因为刚好到第5个)进行排序:
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45
将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].
这时10已经移至正确位置了,然后再以3为步长进行排序:
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45
排序之后变为:
10 14 13 25 23 33 27 25 59 39 65 73 45 94 82 94
最后以1步长进行排序(此时就是简单的插入排序了)。
- 实现代码:
-
static void ShellSort(int[] array) { int n = array.Length; //数组长度 for (int k = n / 2; k >= 1; k/=2) { for (int i = k; i < n; i += k) { int insert_num = array[i]; int j; for (j = i; j > 0 && array[j - k] > insert_num; j -= k) { array[j] = array[j - k]; } //循环最后一次赋值剩余array[j-1],经过 j-=k 后就是最后一个未赋值元素的坐标 array[j] = insert_num; } } }
- 时间复杂度:平均时间复杂度:希尔排序的时间复杂度和其增量序列有关系,这涉及到数学上尚未解决的难题(我承认自己不懂=_=);不过在某些序列中复杂度可以为O(n^1.3)。
- 稳定性:由于增量的原因,序列中元素并不是相邻比较,所以会出现不稳定的现象。举个简单的栗子;3,1,1,4,初始增量为2(数量的一半),分成3,1 和 1,4两组,各自排序再组合结果就会变成1,1,3,4
5.快速排序(QuickSort)
- 定义:快速排序是对冒泡排序的一种本质改进。它的基本思想是通过一趟扫描后,使得排序序列的长度能大幅度地减少。在冒泡排序中,一次扫描只能确保最大数值的数移到正确位置,而待排序序列的长度可能只减少1。快速排序通过一趟扫描,就能确保某个数(以它为基准点吧)的左边各数都比它小,右边各数都比它大。然后又用同样的方法处理它左右两边的数(分治法),直到基准点的左右只有一个元素为止。
- 实现代码:
-
static void QuickSort(int[] array, int left, int right) { int tag; if (left < right) { tag = Partition(array, left, right); QuickSort(array, left, tag - 1); QuickSort(array, tag + 1, right); } } static int Partition(int[] array, int left, int right) { int temp = array[left]; while (left < right) { while (left < right && array[right] >= temp) //直到找到比基值小的数 right--; if (left < right) { array[left] = array[right];} while (left < right && array[left] <= temp) //直到找到比基值大的数 left++; if (left < right) { array[right] = array[left];} } array[left] = temp; return left; }
- 时间复杂度:假设有1到8代表要排序的数,快速排序会递归log(8)=3次,每次对n个数进行一次处理,所以他的时间复杂度为n*log(n)。
- 稳定性:由于算法中从后面开始找比基数小的数,存在不稳定的现象。在元素交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 6, 现在基数元素5和3交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在元素交换的时刻
5.堆排序(HeapSort)
- 定义:堆分为最大堆和最小堆,其实就是完全二叉树。最大堆要求节点的元素都要大于其孩子,最小堆要求节点元素都小于其左右孩子,两者对左右孩子的大小关系不做任何要求。有了上面的定义,我们可以得知,处于最大堆的根节点的元素一定是这个堆中的最大值。其实我们的堆排序算法就是抓住了堆的这一特点,每次都取堆顶的元素,将其放在序列最后面,然后将剩余的元素重新调整为最大堆,依次类推,最终得到排序的序列。或者说,堆排序将所有的待排序数据分为两部分,无序区和有序区。无序区也就是前面的最大堆数据,有序区是每次将堆顶元素放到最后排列而成的序列。每一次堆排序过程都是有序区元素个数增加,无序区元素个数减少的过程。当无序区元素个数为1时,堆排序就完成了。
- 实现代码:
-
static void HeapSort(int[] array) { //序列的长度 int length = array.Length; //先建立一个堆 BuildHeap(array,length); // 取堆顶和最后面的元素交换,剔除堆顶,剩余元素继续成堆,直到剔除所有元素 while (length > 0) { int temp = array[0]; array[0] = array[length - 1]; array[length - 1] = temp; length--; //排除堆顶出去 AdjustHeap(array,length, 0); } } static void BuildHeap(int[] array, int length) { int index = length / 2 - 1; while (index>=0) { AdjustHeap(array, length, index); index--; } } static void AdjustHeap(int[] array, int length, int index) { int left = index * 2 + 1; //左孩子坐标 int right = index * 2 + 2; //右孩子坐标 int largest = index; //父节点坐标 while (left < length || right < length) { if (left < length && array[largest] < array[left]) { largest = left; //记录当前较大值的坐标 } if (right < length && array[largest] < array[right]) { largest = right; //记录当前较大值的坐标 } if (largest != index) { //交换数据 int temp = array[index]; array[index] = array[largest]; array[largest] = temp; index = largest; left = index * 2 + 1 ; right = index * 2 + 2 ; } else { break; } } }
- 时间复杂度:在用数组的实现方式中,每次都把新加入的数值放入数组的末尾。如果这个数比它现在所在位置的父节点的数值小,那么就要把他俩交换。如果在新的位置上还是比它的父节点小,就递归地继续这个过程。这个过程和二叉树的高度成正比,所以是logn。如果把所有n个数都插入进来,那么最坏情况的时间就是O(n*logn)
- 稳定性:堆排序是不稳定的:比如:3 27 36 27,如果堆顶3先输出,则,第三层的27(最后一个27)跑到堆顶,然后堆稳定,继续输出堆顶,是刚才那个27,这样说明后面的27先于第二个位置的27输出,不稳定.
6.总结
排序法 | 最差时间分析 | 平均时间复杂度 | 稳定度 | 空间复杂度 |
冒泡排序 | O(n2) | O(n2) | 稳定 | O(1) |
快速排序 | O(n2) | O(n*log2n) | 不稳定 | O(log2n)~O(n) |
选择排序 | O(n2) | O(n2) | 稳定 | O(1) |
插入排序 | O(n2) | O(n2) | 稳定 | O(1) |
堆排序 | O(n*log2n) | O(n*log2n) | 不稳定 | O(1) |
希尔排序 | —— | —— | 不稳定 | O(1) |