这篇的主题是排序,总结一下各种排序算法,并且比较各排序算法的优缺点,时间复杂度等。
1>比较排序:直接插入排序、希尔排序
2>选择排序:选择排序、堆排序
3>交换排序:冒泡排序、快速排序
4>归并排序
以 { 2, 5, 4, 9, 3, 6, 8, 7, 1, 0 }序列为例进行各种排序的升序算法。
1.直接排序
排序步骤:
{ 2 | 5, 4, 9, 3, 6, 8, 7, 1, 0 }
{ 2, 5 | 4, 9, 3, 6, 8, 7, 1, 0 }
{ 2, 4, 5 | 9, 3, 6, 8, 7, 1, 0 }
{ 2, 4, 5, 9 | 3, 6, 8, 7, 1, 0 }
{ 2, 3, 4, 5, 9 | 6, 8, 7, 1, 0 }
{ 2, 3, 4, 5, 6, 9 | 8, 7, 1, 0 }
{ 2, 3, 4, 5, 6, 8, 9 | 7, 1, 0 }
{ 2, 3, 4, 5, 6, 7, 8, 9 | 1, 0 }
{ 1, 2, 3, 4, 5, 6, 7, 8, 9 | 0 }
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 | }
如上,“|”前面的部分是有序区,后面的部分是无序区,每次将无序区中第一个元素拿出放入有序区中由小到大排列的合适的位置。
void Insert(int *a,size_t n)
{
assert(a);
for(aize_t i=1;i < n -1; ++i)
{
size_t end = i;
int tmp = a[end+1]; //首先进行单趟插入的循环
while(end >= 0)//当end没有越界时,将end位置的数据与无序区要插入进来的数据进行比较
{
if(a[end] > tmp)
{
a[end+1] = a[end];//当新插入的数据比当前数据小的时候,将有序区的数据向后挪,直到插入到合适的位置或者end越界
--end;
}
else
break;
}
}
}
2.希尔排序
希尔排序的思想:(1) 进行预排序,使整个序列接近于有序化
(2) 选择排序(因为序列已经接近于有序,所以排序很节约时间)
选择排序的代码如下:
void ShellSort(int *a, size_t n)
{
assert(a);
size_t gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (size_t i = 0; i < n - gap; i += gap)
{
size_t end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
如上,很显然排序过程中步gap的选择和序列的总元素个数有关,所以在gap=1之前gap由公式gap=gap/3+1给出,gap初值为n;
3.选择排序
主要思想:找出序列中最小的数据,与无序区第一个数据进行交换。
优化:一次选择就找出最大,最小两个数据,同时将最小数据放在前面,将最大数据放在后面
要特别注意一点,当最大数max恰好在begin位置,且最小数min正好在end位置时,只进行一次交换,否则会出错!
代码如下:
void QSort1(int *a, size_t begin, size_t end)
{
size_t min = begin, max = begin;
size_t j = begin;
while (j <= end )//当比较的数据下标没有越界时进入继续比较
{
if (a[j] > a[max])//当当前位置的数据大于max位置处的数据时,变更max
max = j;
++j;
}
j = begin;
while (j <= end)
{
if (a[j] < a[min])
min = j;
++j;
}
if (min == end && max == begin)//当begin位置的数据恰好为最大的数据,且end位置处数据最小时只交换一次
{
swap(a[end], a[begin]);
}
else
{
swap(a[min], a[begin]);
swap(a[max], a[end]);
}
}
4.堆排序
堆排序是一个比较适合表示完全二叉树
堆排序的思想:
1》首先将序列中的元素建大堆的方式建堆。
2》从最后一个结点的父节点开始调节,将父节点与最大的孩子比较,父节点如果小于孩子结点就交换,
3》然后将该孩子结点赋给父节点,继续向下调整,知道调节到的孩子结点已经越界(该位置的元素不存在)
4》直到调节完第一个结点(0号位置上的结点),此时最大的结点已经被调节到0号位置,
5》将最后一个结点与0号位置上的结点进行交换,end变为与0号位置交换的结点的下标
6》调用向下调整算法,调节0号位置的元素,每次调节完后最大的(0~end-1范围内)的元素都会被调节到0号位置处,同理将其和元end-1处的元素交换,直至end=0跳出调节循环
end结点与0号结点交换调节
代码如下:
void AdjustDown(int *a, size_t n, size_t root)
{
size_t parent = root;
size_t child = root * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
if (a[child]>a[parent])
{
swap(a[child], a[parent]);
}
parent = child;
child = parent * 2 + 1;
}
}
void HeapSort(int *a, size_t n)
{
assert(a);
//建大堆
for (int i = (n - 2) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
size_t end = n - 1;
while (end > 0)
{
swap(a[0], a[end]);
AdjustDown(a, end, 0);
--end;
}
}
5.冒泡排序
冒泡排序整体上很简单,只要用两重循环:
1》内循环控制单趟排序
2》外循环控制循环次数
网上关于冒泡排序的讲解很多也很详细,这里就不在复述了,直接展示代码(带优化):
void BubbleSort(int *a, size_t n)
{
assert(a);
for (size_t i = 0; i < n - 1; ++i)
{
bool flag = false;
for (size_t j = 0; j < n - i-1; ++j)
{
if (a[j + 1] < a[j])
{
swap(a[j + 1], a[j]);
flag = true;
}
if (flag == false)
break;
}
}
}
6.快速排序
左右指针法思想:
1》选择一个key值作为区分的准则(一般key取最后一个位置),
2》两个指针begin放在区间的开始,end放在区间的末尾
3》begin找值小于key值的位置,end找大于key值的位置(begin小于end)
4》找到后将begin位置和key位置的数进行交换,并继续查找
5》直至begin和end重合时,则根据编写的函数顺序,此处的值大于key处值,将这两处值交换,
6》此时,以key的值为中心将序列分为小于key值和大于key值两处区间
7》对这两处区间在进行上述1~6操作,直至区间长度为1时则整个序列就已经排好序
代码如下:
size_t PartSort1(int *a, int begin, int end)
{
int key = end;
while (begin < end)
{
while (begin < end && a[begin] <= a[key])
{
++begin;
}
while (begin < end && a[end] >= a[key])
{
--end;
}
swap(a[begin], a[end]);
}
if (begin == end)
{
swap(a[begin], a[key]);
}
return begin;
}
void QuickSort(int *a,int begin,int end)
{
assert(a);
int div = PartSort1(a, begin, end);
if (begin < div-1)
QuickSort(a, begin, div - 1);
if (div + 1 < end)
QuickSort(a, div + 1, end);
}
存在缺陷,当每次取得key处值都为最大或者最小值是,算法的时间复杂度为最坏的O(N*N),所以可以用三数取中法进行优化。
三数取中法:取begin,end,(end+begin)/2三个位置的数进行比较,将大小排在中间的数的下标返回。
三数取中法代码如下:
int GetMidNumber(int *a,int begin,int end)
{
int start = begin;
int finish = end;
int mid = (start + finish) / 2;
if (a[start] < a[finish])
{
if (a[start] < a[mid])
{
if (a[mid] < a[finish])
return mid;
else
return finish;
}
else
return start;
}
else
{
if (a[mid] > a[finish])
{
if (a[mid] > a[start])
return start;
else
return mid;
}
else
return finish;
}
}
前后指针法:
代码如下:
int PartSort3(int *a, int begin, int end)
{
int mid = GetMidNumber(a, begin, end);
swap(a[mid], a[end]);
int cur = begin;
int prev = begin - 1;
while (cur < end)//
{
if (a[cur] < a[end])//当cur处的数小于end处的数
{
++prev; //prev向后挪一位
if (cur < end && prev != cur)//cur还未到end处 并且prev与cur不是指向同一位置(a[cur] < a[end])
{
swap(a[cur], a[prev]);//交换cur位置和prev位置的数据
}
}
++cur;
}
swap(a[++prev], a[end]);
return prev;
}
void QuickSort(int *a,int begin,int end)
{
assert(a);
int div = PartSort3(a, begin, end);
if (begin < div-1)
QuickSort(a, begin, div - 1);
if (div + 1 < end)
QuickSort(a, div + 1, end);
}
7.归并排序
归并排序的思想:
1》将要排序的序列尽量分成长度相等的两部分
2》不断分割,直到最后长度均为1
3》将分割的区域按升序归并
4》一层层归并,直到合成一个序列就已经是有序序列
代码展示如下:
void PartMergeSort(int *a, int begin, int mid, int end, int *tmp)
{
int i = begin, j = mid + 1;
int m = mid, n = end;
int k = 0;
while (i <= m && j <= n)
{
if (a[i] <= a[j])
tmp[k++] = a[i++];
else
tmp[k++] = a[j++];
}
while (i <= m)
{
tmp[k++] = a[i++];
}
while (j <= n)
{
tmp[k++] = a[j++];
}
for (i = 0; i < k;++i)
{
a[begin + i] = tmp[i];
}
}
void _MergeSort(int *a,int begin,int end,int *tmp)
{
if (begin < end)
{
int mid = (begin + end) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
PartMergeSort(a, begin, mid, end, tmp);
}
}
void MergeSort(int *a, int n)
{
assert(a);
int *tmp = new int[n];
if (NULL == tmp)
return;
_MergeSort(a, 0, n-1, tmp);
delete[] tmp;
}
这是比较常用的排序算法的总结,所有代码已经验证无误。