1 插入排序
1.1 直接插入排序
1.1.1 原理分析
1.1.2 代码实现
//直接插入排序
void InsertSort(int *a, size_t n)
{
assert(a);
for (size_t i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (a[end]>tmp)
{
a[end + 1] = a[end];
end--;
}
else
break;
}
a[end + 1] = tmp;
}
}
测试结果:
1.2 希尔排序
1.2.1 原理分析
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
1)插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
2)但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
1.2.2 代码实现
//1.gap>1,预排序
//2.gap=1,直接插入排序
void ShellSort(int *a,size_t n )
{
assert(a);
//预排序
int gap = n;//增量
while (gap >= 1)
{
gap = gap / 3 + 1;
for (size_t i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0 && a[end]>tmp)
{
a[end + gap] = a[end];
end = end - gap;
}
a[end + gap] = tmp;
}
gap--;
}
}
测试结果:
2 选择排序
2.1 选择排序
2.1.1 原理分析
从n个元素中选出最小的放在最左边,选出最大的放在最右边,再对除过最左最右的元素进行上述操作。
2.1.2 代码实现
void SelectSort(int *a, size_t n)
{
assert(a);
int begin = 0;
int end = n - 1;
while (begin < end)
{
int min = begin;
int max = end;
for (int i = begin; i <= end; i++)
{
if (a[i] < a[min])
{
min = i;
}
else if (a[i]>a[max])
{
max = i;
}
}
swap(a[begin], a[min]);
if (max == begin)
{
max = min;
}
swap(a[max], a[end]);
begin++;
end--;
}
}
运行结果:
2.2 堆排序
2.2.1 原理分析
1)将初始待排序关键字序列(R1,R2….Rn)构建成大堆,此堆为初始的无序区
2)将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn)
3)由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成
2.2.2 代码实现
void AdjustDown(int* heap, int k, int root)
{
assert(heap);
for (int i = root; i >= 0; i--)
{
int parent = i;
int child = i * 2 + 1;
while (child < k)
{
if (child+1<k&&heap[child]<heap[child + 1])
{
child++;
}
if (heap[parent]<heap[child])
{
swap(heap[parent], heap[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
}
void HeapSort(int* a, int n)
{
assert(a);
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
swap(a[0],a[end]);
AdjustDown(a, end, 0);
end--;
}
}
3 交换排序
3.1 冒泡排序
3.1.1 原理分析
冒泡排序:依次比较相邻的数据,将最大的数据放在最后;即第一趟先比较第1个和第2个数,大数在后,小数在前,再比较第2个数与第3个数,大数在后,小数在前,以此类推则将最大的数”滚动”到最后一个位置;第二趟则将次大的数滚动到倒数第二个位置……第n-1(n为无序数据的个数)趟即能完成排序。
优化方法:对冒泡排序算法进行简单的优化,用一个标记来记录在一趟的比较过程中是否存在交换,如果不存在交换则整个数组已经有序退出排序过程,反之则继续进行下一趟的比较。
3.1.2 代码实现
void BubbleSort(int* a, int n)
{
assert(a);
for (int i = 0; i < n - 1; ++i)
{
for(int j = 0; j < n - 1 - i; ++j)
{
if (a[j]>a[j + 1])
{
swap(a[j], a[j + 1]);
}
}
}
}
运行结果:
优化后的:
void BubbleSort(int* a, int n)
{
assert(a);
bool flag = true;
for (int i = 0; i < n - 1; ++i)
{
for(int j = 0; j < n - 1 - i; ++j)
{
if (a[j]>a[j + 1])
{
flag = false;
swap(a[j], a[j + 1]);
}
}
if (flag == true)
break;
}
}
3.2 快速排序
快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
步骤:
- 1)先从数列中取出一个数作为基准数
- 2)分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
- 3)再对左右区间重复第二步,直到各区间只有一个数
快速排序的三种方法:1)左右指针法;2)挖坑法;3)前后指针法
递归方法:
3.2.1 左右指针法
int PartSort1(int* a, int left, int right)
{
assert(a != NULL);
int start = left;
int end = right;
int div = a[end];
while (start < end)
{
while (start < end&&a[start] <= div)
{
start++;
}
while (start < end&&a[end] >= div)
{
end--;
}
swap(a[start], a[end]);
}
swap(a[start], a[right]);
return start;
}
void QuickSort(int* a, int left,int right)
{
if (left >= right)
{
return;
}
int div = PartSort1(a, left, right);
if (left<div-1)
QuickSort(a, left, div - 1);
if (div+1<right)
QuickSort(a, div + 1, right);
}
3.2.2 挖坑法
int PartSort2(int* a, int left, int right)
{
assert(a != NULL);
int start = left;
int end = right;
int div = a[end];
while (start < end)
{
while (start < end&&a[start] <= div)
{
start++;
}
if (start < end)
a[end--] = a[start];
while (start < end&&a[end] >= div)
{
end--;
}
if (start < end)
a[start++] = a[end];
}
a[start] = div;
return start;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int div = PartSort2(a, left, right);
if (left < div - 1)
QuickSort(a, left, div - 1);
if (div + 1 < right)
QuickSort(a, div + 1, right);
}
3.2.3 前后指针法
int PartSort3(int* a, int left, int right)
{
assert(a != NULL);
int div = a[right];
int cur = left;
int prev = left-1;
while (cur < right)
{
if (a[cur] <= div&&++prev != cur)
swap(a[cur], a[prev]);
cur++;
}
if (a[++prev]!=div)
swap(a[prev], a[right]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int div = PartSort3(a, left, right);
if (left < div - 1)
QuickSort(a, left, div - 1);
if (div + 1 < right)
QuickSort(a, div + 1, right);
}
3.2.4 优化方法:三数取中法
快速排序是一种交换类的排序,它同样是分治法的经典体现。在一趟排序中将待排序的序列分割成两组,其中一部分记录的关键字均小于另一部分。然后分别对这两组继续进行排序,以使整个序列有序。在分割的过程中,枢纽值的选择至关重要,本文采取了三位取中法,可以很大程度上避免分组”一边倒”的情况。快速排序平均时间复杂度也为O(nlogn)
int GetMidIndex(int* a, int left, int right)
{
assert(a);
if (left >= right)
return -1;
int mid = left + ((right - left) >> 1);
if (a[right] > a[left])
{
if (a[right] < a[mid])
{
return right;
}
else if (a[left] > a[mid])
{
return left;
}
else
{
return mid;
}
}
else
{
if (a[mid] > a[left])
{
return left;
}
else if (a[right] > a[mid])
{
return right;
}
else
{
return mid;
}
}
}
int PartSort(int* a, int left, int right)
{
assert(a != NULL);
int div = GetMidIndex(a,left,right);
int key = a[div];
swap(a[div], a[right]);
int cur = left;
int prev = left-1;
while (cur < right)
{
if (a[cur] <= key&&++prev != cur)
{
swap(a[cur], a[prev]);
}
cur++;
}
swap(a[++prev], a[right]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
//当区域很小的时候,插入排序比快排递归效率高
if (right - left <= 20)
{
InsertSort(a + left, right - left + 1);
}
int div = PartSort(a, left, right);
if (left < div - 1)
QuickSort(a, left, div - 1);
if (div + 1 < right)
QuickSort(a, div + 1, right);
}
3.2.5 非递归法
int PartSort1(int* a, int left, int right)
{
assert(a != NULL);
int start = left;
int end = right;
int div = a[end];
while (start < end)
{
while (start < end&&a[start] <= div)
{
start++;
}
while (start < end&&a[end] >= div)
{
end--;
}
swap(a[start], a[end]);
}
swap(a[start], a[right]);
return start;
}
void QuickSortNOR(int* a, int left, int right)
{
stack<int> S;
if (left < right)
{
S.push(right);
S.push(left);
}
while (!S.empty())
{
int start = S.top();
S.pop();
int end = S.top();
S.pop();
int div = PartSort1(a, start, end);
if (start < div - 1)
{
S.push(div-1);
S.push(start);
}
if (div + 1 < end)
{
S.push(end);
S.push(div + 1);
}
}
}
4 归并排序
4.1 原理
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案”修补”在一起,即分而治之)。
4.2 代码实现
void _MergeSort(int* a,int left,int right,vector<int> tmp)
{
if (left >= right)
{
return;
}
assert(a);
int mid = left + ((right - left) >> 1);
//划分子区间
_MergeSort(a, left, mid,tmp);
_MergeSort(a, mid+1,right,tmp);
//归并
int begin1 = left;
int end1 = mid;
int begin2 = mid + 1;
int end2 = right;
while (begin1 <= end1&&begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp.push_back(a[begin1]);
begin1++;
}
else
{
tmp.push_back(a[begin2]);
begin2++;
}
}
while (begin1 <= end1)
{
tmp.push_back(a[begin1]);
begin1++;
}
while (begin2 <= end2)
{
tmp.push_back(a[begin2]);
begin2++;
}
int index = 0;
while (left <= right){
a[left++] = tmp[index++];
}
}
5 非比较排序(计数排序和基数排序)
5.1 计数排序
5.1.1 原理
给定一组要排序的序列,找出这组序列中的最大值,然后开辟一个最大值加1大小的数组,将这个数组里面的元素全部置零,然后用这个数组统计出要排序的序列中各个元素出现的次数。等到统计完成的时候,排序就已经完成了。
算法优化:
计数排序适用于数据比较集中的序列排序。
如果是序列56 ,77,82,则开辟的数组下标56以前的空间就浪费了,基于节省空间的角度进行考虑,可以开辟更小的临时数组统计次数。
方法:找出要排序的这组元素中的最大值和最小值,这样就确定了这组元素的范围,然后开辟这个范围加1大小的数组,然后再将要排序的元素映射到这个新开辟的数组中就可以了。
5.1.2 代码实现
简单实现:
int GetMaxNum(int* a, int n)
{
assert(a != NULL);
int ret = a[0];
for (int i = 1; i < n; i++)
{
if (a[i]>ret)
{
ret = a[i];
}
}
return ret;
}
void Count_Sort(int* a, int n)
{
assert(a != NULL);
int Max = GetMaxNum(a, n);
int * tmp = new int[Max + 1];
memset(tmp, 0, sizeof(int)*(Max + 1));
for (int i = 0; i < n; i++)
{
tmp[a[i]]++;
}
int index = 0;
for (int i = 0; i < Max + 1; i++)
{
while (tmp[i] != 0)
{
a[index++] = i;
tmp[i]--;
}
}
delete tmp;
}
优化代码实现:
int GetLength(int* a, int n)
{
assert(a != NULL);
int ret = 0;
int max = 0;
int min = 0;
for (int i = 1; i < n; i++)
{
if (a[i]>a[max])
{
max = i;
}
if (a[i] < a[min])
{
min = i;
}
}
ret = a[max] - a[min]+1;
return ret;
}
void Count_Sort(int* a, int n)
{
assert(a != NULL);
int len = GetLength(a, n);
int * tmp = new int[len];
memset(tmp, 0, sizeof(int)*len);
for (int i = 0; i < n; i++)
{
tmp[a[i]]++;
}
int index = 0;
for (int i = 0; i < len; i++)
{
while (tmp[i] != 0)
{
a[index++] = i;
tmp[i]--;
}
}
delete tmp;
}
5.2 基数排序
5.2.1 原理
构建一个足够大的数组Array[],数组大小需要保证能够把要排序的所有元素都包含在这个数组上 。
从上述可以看到,分别根据数据的个位,十位等进行排序,可知数据排序次数为数据的最大长度。
优化方法:
以一张二维数组的表来存储这些无序的元素,使用二维数组有一个很明显的不足就是二维数组太过稀疏。数组的利用率为 低。
可用一种压缩空间的方法,且时间复杂度并没有偏离得太厉害。设计两个辅助数组,一个是 count[],一个是 bucket[]。count 用于记录数据在 bucket[]中的的下标,然后再计算属于bucket[]的位置,并修改相应位置的 count 值。
5.2.2 代码实现
#define N 10
int GetMaxbit(int* a, int n)//求数组中最大位数
{
int bit = 1;
int p = 10;
for (int i = 0; i < n; i++)
{
if (a[i] >= p)
{
bit++;
p = p * 10;
}
}
return bit;
}
void CountSort(int* a, int n)
{
if (a == NULL||n<1)
{
return;
}
int bit = GetMaxbit(a, n);
int count[10];
int tmp[N];
int radix = 1;
for (int i = 1; i <= bit; ++i)
{
for (int j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for (int j = 0; j < n; j++)
{
int index = (a[j] / radix) % 10;
count[index]++;
}
for (int j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中a[j]的位置
for (int j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
int index = (a[j] / radix) % 10;
tmp[count[index] - 1] = a[j];
count[index]--;
}
for (int j = 0; j < n; j++) //将临时数组的内容复制到data中
a[j] = tmp[j];
radix = radix * 10;
}
}
总:排序算法性能分析
类别 | 排序方法 | 时间复杂度(平均情况) | 时间复杂度(最好情况) | 时间复杂度(最坏情况) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
插入排序 | 插入排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
插入排序 | shell排序 | O(N^1.3) | O(N) | O(N^2) | O(1) | 不稳定 |
选择排序 | 选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
选择排序 | 堆排序 | O(N* logN) | O(N *logN) | O(N *logN) | O(1) | 不稳定 |
交换排序 | 冒泡排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
交换排序 | 快速排序 | O(N* logN) | O(N *logN) | O(N *2) | O(logN) | 不稳定 |
归并排序 | 归并排序 | O(N* logN) | O(N *logN) | O(N *logN) | O(N) | 稳定 |
计数排序:时间复杂度:O(N), 空间复杂度O(最大数-最小数)
基数排序:时间复杂度:O(N*位数),空间辅助度O(N)