排序算法
排序基本概念与分类
将数据集进行排序,如果数据本身不可以排序,可以使用hash函数建立一个顺序列,映射到整数集,然后再进行排序。
- 内排序:待排序的数据全部存储在内存当中,很多排序算法都是内排序
- 外排序:存储在磁带和磁盘的数据叫做外排序,归并排序是外排的基础。
- 原地排序算法:不需要额外的空间,只需要几个额外的中间变量。
- 时间复杂度:最优就是O(nlogn),最差就是O(n2),
- 稳定性:相同的数据元素排序前后的位置是不改变,这样的叫做稳定排序,而前后顺序发生了改变就是不稳定的。
- 适应性:如果一个排序算法对接近有序的序列工作的更快,就称这种算法具有适用性。
1. 插入排序
思路:
类似与扑克牌摸牌,将一个记录插入到已经排好序的有序表中,从而得到一个新的有序表。
for 第二个元素 to 最后一个元素:
while 插入元素 > 前一个元素 and 前一个元素不为空:
前一个元素后移
将该元素插入到当前位置
代码:
// C
void InsertSort(int *nums, int len)
{
int i, j;
int temp;
for (i = 1; i < len; i++) {
temp = nums[i];
j = i;
while (j > 0 && nums[j-1] > temp) {
nums[j] = nums[j-1];
j--;
}
nums[j] = temp;
}
}
#python
def insert_method(lst):
for i in range(1, len(lst)):
j = i
x = lst[i]
while j > 0 and lst[j-1] > x:
lst[j] = lst[j-1]
j -= 1
lst[j] = x
return lst
特点:
最好时间复杂度为O(n),情况发生在数组本来就是从小到大排列。
项目 | 最坏 | 最好 | 平均 | 空间复杂度 | 稳定性 | 适应性 |
---|---|---|---|---|---|---|
插入排序 | n2 | n | n2 | 1 | 稳定 | 有 |
2. 选择排序
思路:
找到合适的关键字再做交换,并且只移动一次就完成,每次循环都要找到最小的元素放到合适的位置。
for 第一个元素(i) to 倒数第二个元素:
for 第二个元素 to 最后一个元素:
记录最小的元素值
最小值赋值给i
代码:
// C
void SelectSort(int *nums, int len)
{
int i, j, x;
int temp;
for (i = 0; i < len; i++) {
x = i;
for (j = i + 1; j < len; j++) {
if (nums[j] < nums[x]) {
x = j;
}
}
if (x != i) {
temp = nums[i];
nums[i] = nums[x];
nums[x] = temp;
}
}
}
#python
def select_method(lst):
for i in range(0, len(lst)-1):
x = i
for j in range(i+1, len(lst)):
if lst[j] < lst[x]:
x = j
if x != i:
lst[i], lst[x] = lst[x], lst[i]
return lst
特点:
此算法的平均以及最坏时间复杂度都是o(n^2),每一次循环都要从i到尾进行比较。所以算法没有适应性。从头到尾扫描最小值,这样相同关键码的值肯定要是不会破坏顺序的。但是如果是3,3,1。这样的,一和三交换就破环了稳定性,所以算法是不稳定的
项目 | 最坏 | 最好 | 平均 | 空间复杂度 | 稳定性 | 适应性 |
---|---|---|---|---|---|---|
插入排序 | n2 | n2 | n2 | 1 | 不稳定 | 无 |
3. 希尔排序
思路:
该排序方式是第一批突破O(n2)时间复杂度的算法之一,中心思想是将待排序数组,分为几个子序列,保证子序列之间大致是按照从小到大的趋势。拆分的方法是选取一个跨度值gap,按照相同跨度值元素归为一个子序列,如{9,1,5,8,3,7,4}选取跨度为2时,则分为{9,5,3,4}、{1,8,7}两组,然后两组分别采用插入排序的方法保证子序列有序,然后逐步减小跨度gap的值,直到为1最后一次排序,由于数组已经接近有序,所以插入排序就很高效。
while 跨度gap > 0:
for 下标为gap的元素 to 最后一个元素:
插入排序的算法保证跨度gap的子序列有序
gaps 缩小 2倍
代码:
// c
void ShellSort(int *nums, int len)
{
int gap, i, j , temp;
gap = len / 2;
while (gap > 0) {
for (i = gap; i < len; i++) {
if (nums[i-gap] > nums[i]) {
temp = nums[i];
for (j = i-gap; j >= 0 && nums[j] > temp; j -= gap) {
nums[j+gap] = nums[j];
}
nums[j+gap] = temp;
}
}
gap /= 2;
}
}
#python
def shell_sort(lst):
n = len(lst)
gap = n//2
while gap > 0:
for i in range(gap, n):
j = i
while j - gap >= 0 and lst[j-gap] > lst[j]:
lst[j-gap], t[j] = lst[j], lst[j-gap]
j -= gap
gap = gap // 2
return lst
特点:
该算法的排序是基于gap值的选取,gap为一个增量序列,因此其时间复杂度为O(n3/2),比通常的O(n2)要好,但是必须注意,增量序列的最后一个值必须为1,并且由于排序是跳跃式的,因此不是稳定排序。
)项目 | 最坏 | 最好 | 平均 | 空间复杂度 | 稳定性 | 适应性 |
---|---|---|---|---|---|---|
希尔排序 | n2 | n | n3/2 | 1 | 不稳定 | 有 |
4. 堆排序
思路:
将待排序的序列构造成一个大堆顶,每次将大堆顶的第一个最大元素取出,存到序列的最后一位,然后再把前n-1个元素重新构造成大堆顶。
该过过程需要两个步骤:
1. 将序列的构造成一个大堆顶;
2. 每次取出顶部元素以后,重新恢复大堆顶。
代码:
void siftdown(int *nums, int begin, int end)
{
int i, j, temp;
i = begin;
j = 2 * i + 1;
temp = nums[i];
while (j < end) {
if ((j + 1) < end && nums[j+1] > nums[j]) {
j = j + 1;
}
if (nums[j] < temp) {
break;
}
nums[i] = nums[j];
i = j;
j = 2 * j + 1;
}
nums[i] = temp;
}
void HeapSort(int *nums, int len)
{
int i, temp;
for (i = len / 2; i >= 0; i--) {
siftdown(nums, i, len);
}
for (i = len-1; i >= 0; i--) {
temp = nums[i];
nums[i] = nums[0];
nums[0] = temp;
siftdown(nums, 0, i);
}
}
#python
def heap_method(lst):
def siftdown(lst, e, begin, end):
i, j = begin, begin*2+1
while j < end:
if j+1 < end and lst[j+1] < lst[j]:
j += 1
if lst[j] > e:
break
lst[i] = lst[j]
i, j = j, 2 * j + 1
lst[i] = e
end = len(lst)
for i in range(end//2-1, -1, -1):
siftdown(lst, lst[i], i, end)
for i in range(end-1, 0, -1):
e = lst[i]
lst[i] = lst[0]
siftdown(lst, e, 0, i)
return lst
特点:
该算法的排序主要消耗是初始化建堆时和重构堆的反复筛选上,在建堆的时候,是从最下层最右端的元素开始,外层需要循环n次,然后进入筛选函数中,因为已经保证底层是大堆顶了,因此函数内部只需要判断一次,和左右节点判断选出最大的元素即可,因次筛选函数内部只执行O(1)次,整体构建时需要O(n)的时间复杂度。
在正式排序时,第i次取堆顶记录重建大堆顶需要O(logn)次,外层
一共需要循环n次,因此排序时间复杂对为O(nlogn)。
项目 | 最坏 | 最好 | 平均 | 空间复杂度 | 稳定性 | 适应性 |
---|---|---|---|---|---|---|
堆排序 | nlogn | nlogn | nlogn | 1 | 不稳定 | 无 |
5. 冒泡排序
思路:
通过两两比较的方法,把最大的元素依次交换到最后的位置,接着依次循环把大的值往后交换,可以在一定程度上优化排序算法的复杂度。
代码:
void BubbleSort(int *nums, int len)
{
int i, j, temp;
int find;
for (i = 0; i < len - 1; i++) {
find = 0;
for (j = 0; j < len - i - 1; j++) {
if (nums[j] > nums[j+1]) {
temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
find = 1;
}
}
if (find == 0) {
break;
}
}
}
#python
def bubble_method(lst):
for i in range(len(lst)-1):
found = False
for j in range(len(lst)-1-i):
if lst[j] > lst[j+1]:
lst[j], lst[j+1] = lst[j+1], lst[j]
found = True
if not found:
break
return lst
特点:
该排序的方法,是两两元素之间进行比较,每一次循环,如果没有遍历一遍没有发现存在逆序元素,则代表序列已经是有序的了,则这时通过标志位跳出循环,可以降低算法的时间复杂度,在序列已经是排好序时,则效率最快,序列为倒序要排位正序时,效率最慢。
项目 | 最坏 | 最好 | 平均 | 空间复杂度 | 稳定性 | 适应性 |
---|---|---|---|---|---|---|
冒泡排序 | n2 | n | n2 | 1 | 稳定 | 有 |
6. 快速排序
思路:
选取第一个元素作为中间元素,将待排序的序列分割成为独立的两部分,中间元素左侧的所有值要小,中间元素右侧值都要大于中间值,接着分别对两部分进行排序,只需要细分logn次,内部比较n次,即可完成排序。算法整体采用递归的思想。
代码:
void QuickSortCore(int * nums, int begin, int end)
{
int i, j, temp;
i = begin;
j = end;
if (i >= j) {
return;
}
temp = nums[i];
while (i < j) {
while ((i < j) && (nums[j] >= temp)) {
j--;
}
if (i < j) {
nums[i] = nums[j];
i++;
}
while ((i < j) && (nums[i] <= temp)) {
i++;
}
if (i < j) {
nums[j] = nums[i];
j--;
}
}
nums[i] = temp;
QuickSortCore(nums, begin, i-1);
QuickSortCore(nums, i+1, end);
}
void QuickSort(int *nums, int len)
{
QuickSortCore(nums, 0, len - 1);
}
def quick_sort(lst):
def qsort_method(lst, begin, end):
if begin >= end:
return
i = begin
j = end
dummy = lst[i]
while i < j:
while i < j and lst[j] > dummy:
j -= 1
if i < j:
lst[i] = lst[j]
i += 1
while i < j and lst[i] < dummy:
i += 1
if i < j:
lst[j] = lst[i]
j -= 1
lst[i] = dummy
qsort_method(lst, begin, i-1)
qsort_method(lst, i+1, end)
qsort_method(lst, 0, len(lst)-1)
return lst
特点:
抽象的看,快速排序产生的划分结构,可以看作以中间元素为根,两边的分别作为左子树和右子树进行再排序,可以类比为左右子树的二叉树,二叉树的平均高度为logn。越是接近排好序的序列,算法效率越低。由于递归情况的存在,会造成栈空间的使用,最好的情况,递归树的高度为logn,最坏情况为O(n),平均为O(logn)。算法没有适应性,反而越有序的适得其反。同时算法也是不稳定的。
项目 | 最坏 | 最好 | 平均 | 空间复杂度 | 稳定性 | 适应性 |
---|---|---|---|---|---|---|
快速排序 | n2 | nlogn | nlogn | logn~n(递归次数) | 不稳定 | 无 |
7. 归并排序
思路:
类似于二叉树的倒置,将数组一直拆分为每一个元素,接着两两排序合并,存放在临时变量数组中,然后将临时数组中的值拷贝到原数组中,如此重复下去,直到所有分成两段的数组都合并为同一个有序数组。
代码:
/* 归并排序 */
void Merge(int *nums, int start, int mid, int end)
{
int numsTemp[end-start+1];
int i, j, o = 0;
i = start;
j = mid + 1;
while (i <= mid && j <= end) {
if (nums[i] > nums[j]) {
numsTemp[o++] = nums[j++];
} else {
numsTemp[o++] = nums[i++];
}
}
while (i <= mid) {
numsTemp[o++] = nums[i++];
}
while (j <= end) {
numsTemp[o++] = nums[j++];
}
for (o = 0; o < end - start + 1; o++) {
nums[o + start] = numsTemp[o];
}
}
void MergeSortDiv(int *nums, int start, int end)
{
int mid;
if (start < end) {
mid = (start + end) / 2;
MergeSortDiv(nums, start, mid);
MergeSortDiv(nums, mid+1, end);
Merge(nums, start, mid, end);
}
}
void MergeSort(int *nums, int len)
{
int start, end;
start = 0;
end = len - 1;
MergeSortDiv(nums, start, end);
}
#python
def merge_sort(lst):
def merge(lfrom, lto, low, mid, high):
i, j, k = low, mid, low
while i < mid and j < high:
if lfrom[i] <= lfrom[j]:
lto[k] = lfrom[i]
i += 1
else:
lto[k] = lfrom[j]
j += 1
k += 1
while i < mid:
lto[k] = lfrom[i]
i += 1
k += 1
while j < high:
lto[k] = lfrom[j]
j += 1
k += 1
def merge_pass(lfrom, lto, llen, slen):
i = 0
while i + 2 * slen < llen:
merge(lfrom, lto, i, i+slen, i+2*slen)
i += 2 * slen
if i +slen < llen:
merge(lfrom, lto, i, i+slen, llen)
else:
for j in range(i, llen):
lto[j] = lfrom[j]
slen, llen = 1, len(lst)
template = [None] * llen
while slen < llen:
merge_pass(lst, template, llen, slen)
slen *= 2
merge_pass(template, lst, llen, slen)
slen *= 2
return lst
特点:
抽象的看,归并排序类似二叉树的倒置,因此时间复杂度和树的高度有关,每一趟归并,需要将数组从头到尾扫描一遍,时间复杂度为O(n),树的高度为logn,因此时间复杂度为O(nlogn)。由于归并在过程中需要临时变量数组存放元素,并且需要递归深度为logn,因此空间复杂度为O(n+logn),归并是两两比较因此是稳定的,比较栈用内存但是是效率高且稳定的算法。
项目 | 最坏 | 最好 | 平均 | 空间复杂度 | 稳定性 | 适应性 |
---|---|---|---|---|---|---|
归并排序 | nlogn | nlogn | nlogn | n+logn | 稳定 | 无 |
8. 代码示例
项目 | 链接 |
---|---|
C | Github |
python | Github |