排序的概念
在实现排序算法之前,我们需要对排序的相关概念有一个清楚的理解。
排序:排序就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假设在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序之后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的,否则称为不稳定的。
内部排序:数据元素全部在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
八大排序算法
1.插入排序
直接插入排序
希尔排序
-
直接插入排序:
基本思想:依次将待排序的记录和前面已经排好序的记录相比较,如果选择的元素比已排好序的元素还小,就交换两个位置的值,直到全部元素都比较完。
代码实现:
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int cur = i;
int temp = a[cur + 1];
while (cur >= 0)
{
if (a[cur] > temp)
{
a[cur + 1] = a[cur];
a[cur] = temp;
--cur;
}
else
{
break;
}
}
}
}
-
希尔排序
基本思想:希尔排序也叫缩小增量排序。其思想是:把记录按下标的一定增量进行分组,对每组使用直接插入排序,每排完一组,增量递减,当增量减至1时,进行最后一次直接插入排序。增量gap一般控制在gap = gap/3+1递减。
代码实现:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int temp = a[end + gap];
while (end >= 0 && a[end] > temp)
{
a[end + gap] = a[end];
end -= gap;
}
a[end + gap] = temp;
}
}
}
2.选择排序
- 直接选择排序
- 堆排序
-
直接选择排序
基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
代码实现:
void SelectSort(int* a,int n)
{
for(int i = 0; i < n - 1; i++)
{
int minindex = i;
for(int j = i + 1; j < n; j++)
{
if(a[minindex] > a[j])
{
minindex = j;
}
}
if(minindex != i)
{
int tmp = a[minindex];
a[minindex] = a[i];
a[i] = tmp;
}
}
}
-
堆排序
基本思想:利用堆这种数据结构所设计的一种排序算法,通过堆来进行选择数据,需要注意的是升序建大堆,降序建小堆。
实现代码:
void Swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
void Print(int* a, int n)
{
for (int i = 0; i < n; i++)
{
cout << a[i] << " ";
}
cout << endl;
}
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child <= n - 1)
{
if (child + 1 < n && a[child] < a[child+1])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//排升序建大堆
void HeapSort(int* a,int n)
{
for (int i = ((n - 1) - 1) / 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;
}
Print(a, n);
}
void TestHeapSort()
{
int a[] = { 20, 17, 4, 16, 5, 3 };
int n = sizeof(a) / sizeof(a[0]);
HeapSort(a, n);
}
3.交换排序
交换就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
- 冒泡排序
- 快速排序
-
冒泡排序
基本思想:每轮比较中将最大的元素沉入数组尾部。
代码实现:
void Swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
void Print(int* a, int n)
{
for (int i = 0; i < n; i++)
{
cout << a[i] << " ";
}
cout << endl;
}
void BubbleSort(int* a, int n)
{
int end = n;
while (end > 0)
{
int flag = 0;
for (int i = 0; i < end - 1; i++)
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
flag = 1;
}
}
if (0 == flag)
{
break;
}
--end;
}
Print(a, n);
}
void TestBubbleSort()
{
int a[] = { 15, 6, 56, 24, 9, 12, 55 };
int n = sizeof(a) / sizeof(a[0]);
BubbleSort(a, n);
}
-
快速排序
基本思想:任取待排元素序列中的某元素作为基准值,按照该排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右序列重复该过程,直到所有元素都排列在相应位置上为止。
将区间按照基准值会分为左右两半部分的常见方式有:
- hoare版本
- 挖坑法
- 前后指针版本
hoare版本:
1.选key下标对应的值作为基准值;
2.从数组的最右边end向左出发,找到第一个比基准值小的值的位置;
3.然后再从数组的最左边begin向右出发,找到第一个比基准值大的值的位置;
4.然后交换这两个位置上的值。
5.重复上面的步骤,直到begin和end相遇结束,将基准值与此时begin对应的值交换。
代码实现:
void Swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int PartSort1(int*a, int begin, int end)
{
int key = begin;
while (begin < end)
{
while (begin < end && a[key] <= a[end])
{
--end;
}
while (begin < end && a[key] >= a[begin])
{
++begin;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[key]);
return begin;
}
挖坑法:
1.定义两个指针begin指向起始位置,end指向最后一个元素的位置,将第一个位置的值指定为基准值,并作为坑;
2.从end向前找比基准值小的元素,找到后将end位置上的数值赋给此时begin位置上的元素;
3.再从begin位置向后找比基准值大的元素,找到后将begin位置上的数值赋给此时end位置上的元素;
4.重复上面步骤,直到begin和end相遇,此时将基准值赋给相遇位置。
代码实现:
void Swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int PartSort2(int* a, int begin, int end)
{
int key = a[begin];
while (begin < end)
{
while (begin < end && key <= a[end])
{
--end;
}
a[begin] = a[end];
while (begin < end && key >= a[begin])
{
++begin;
}
a[end] = a[begin];
}
//此时begin和end相遇
a[begin] = key;
return begin;
}
前后指针法:
1.定义两个指针cur和prev,cur在前指向起始位置的下一个位置,prev在后指向起始位置,并选择起始位置的数值作为基准值key。
2.cur找比key小的数,prev在cur没有找到的情况下一直不动。
3.cur找到比key小的数之后,如果此时++prev != cur就交换prev和cur上的数值。然后cur自增,重复上述步骤;
4.当cur走到数组末尾的下一个位置时,结束循环。
实现代码:
void Swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int PartSort3(int* a, int begin, int end)
{
int key = a[begin];
int cur = begin;
int prev = begin - 1;
while (cur <= end)
{
if (a[cur] < key && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
++prev;
Swap(&a[begin], &a[prev]);
return prev;
}
快速排序的优化
在数组有序或接近有序的时候,上面的方法效率会很低,我们可以采用三数取中法来进行优化。三数取中法就是在待排序序列中比较第一个元素、中间元素和最后一个元素的大小,然后取其中间大小的元素。
int GetMidIndex(int* a, int begin, int end)
{
int mid = begin + ((end - begin) >> 1);
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return begin;
}
else
{
return end;
}
}
else
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return begin;
}
else
{
return end;
}
}
}
在递归子区间的时候,如果区间内的数据比较少的时候我们就不用再划分子区间,而是直接用直接插入排序效率更高。因为划分子区间又要创建栈帧的开销。
优化后的快速排序
int GetMidIndex(int* a, int begin, int end)
{
int mid = begin + ((end - begin) >> 1);
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return begin;
}
else
{
return end;
}
}
else
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return begin;
}
else
{
return end;
}
}
}
//1.hoare版本
int PartSort1(int*a, int begin, int end)
{
int mid = begin + ((end - begin) >> 1);
mid = GetMidIndex(a, begin, end);
if (mid != begin)
{
Swap(&a[mid], &a[begin]);
}
int key = begin;
while (begin < end)
{
while (begin < end && a[key] <= a[end])
{
--end;
}
while (begin < end && a[key] >= a[begin])
{
++begin;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[key]);
return begin;
}
//2.挖坑法
int PartSort2(int* a, int begin, int end)
{
int mid = begin + ((end - begin) >> 1);
mid = GetMidIndex(a, begin, end);
if (mid != begin)
{
Swap(&a[mid], &a[begin]);
}
int key = a[begin];
while (begin < end)
{
while (begin < end && key <= a[end])
{
--end;
}
a[begin] = a[end];
while (begin < end && key >= a[begin])
{
++begin;
}
a[end] = a[begin];
}
//此时begin和end相遇
a[begin] = key;
return begin;
}
//3.前后指针法
int PartSort3(int* a, int begin, int end)
{
int mid = begin + ((end - begin) >> 1);
mid = GetMidIndex(a, begin, end);
if (mid != begin)
{
Swap(&a[mid], &a[begin]);
}
int key = a[begin];
int cur = begin + 1;
int prev = begin;
while (cur <= end)
{
if (a[cur] < key && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[begin], &a[prev]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
if ((right - left + 1) < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
//int div = PartSort1(a, left, right);
//int div = PartSort2(a, left, right);
int div = PartSort3(a, left, right);
//递归去div的左区间找新的关键字
QuickSort(a, left, div - 1);
//递归去div的右区间找新的关键字
QuickSort(a, div + 1, right);
}
}
快速排序(非递归版本)
利用栈保存数组左右区间的边界元素。
1.左右区间的元素先入栈;
2.然后再取出栈顶元素,出栈;
3.然后可以选择以上任意一种快排算法进行排序。
4.入栈,左右区间的边界元素,循环上述步骤,直到栈为空。
注意:进栈出栈的顺序,如果先进的是左边界元素,第一个出栈的是右边界的元素,不要弄反了。
代码实现:
void Swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
//3.前后指针法
int PartSort3(int* a, int begin, int end)
{
int mid = begin + ((end - begin) >> 1);
mid = GetMidIndex(a, begin, end);
if (mid != begin)
{
Swap(&a[mid], &a[begin]);
}
int key = a[begin];
int cur = begin + 1;
int prev = begin;
while (cur <= end)
{
if (a[cur] < key && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[begin], &a[prev]);
return prev;
}
//实现一个快速排序的非递归算法
void QuickSortNOR(int* a, int left, int right)
{
std::stack<int> s;
if (left < right)
{
s.push(left);
s.push(right);
}
while (!s.empty())
{
int end = s.top();
s.pop();
int begin = s.top();
s.pop();
int div = PartSort3(a, begin, end);
if ((end - begin + 1) < 10)//小区间优化直接插入排序
{
InsertSort(a + left, right - left + 1);
}
else
{
if (begin < div - 1)
{
s.push(begin);
s.push(div - 1);
}
if (div + 1 < end)
{
s.push(div + 1);
s.push(end);
}
}
}
}
4.归并排序
归并排序算法的主要思想就是分治法。每个递归过程涉及到分解和合并。
分解:将待排序列n个元素分解成两个子序列,每个子序列包含n/2个元素。
合并:将两个排好的子序列进行合并
代码实现:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end) //退出条件
return;
int mid = begin + ((end - begin) >> 1);
_MergeSort(a, begin, mid, tmp); // 递归左半数组
_MergeSort(a, mid + 1, end, tmp); // 递归右半数组
//将排好序的两部分数组归并(排序)
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin;
while (begin1 <= end1 && begin2 <= end2)// 循环条件:任一个数组排序完,则终止条件,最后将没有比较完的数组直接一一拷过去
{
if (a[begin1] <= a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
if (begin1 <= end1)
{
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
}
else
{
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
}
//将tmp中的数据拷贝到原数组中
memcpy(a+begin, tmp+begin, sizeof(int)*(end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = new int[n];
_MergeSort(a, 0, n - 1, tmp);
delete[] tmp;
}
5.计数排序
计数排序是额外开一个范围大小的数组,该范围是待排序数组中元素的最大值和最小值之差加一,然后将新开数组的每个位置的值初始化为0,遍历原数组,统计每个数出现的次数,最后遍历范围进行排序。
代码实现:
void CountSort(int* a,int n)
{
int max = a[0];
int min = a[0];
//循环找到原数组中的最大值和最小值
for (int i = 0; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;
int* counta = new int[range];
memset(counta, 0, sizeof(int)*range);//将新数组counta中的每个元组初始化为0
//遍历原数组,统计每个数出现的次数
for (int i = 0; i < n; i++)
{
counta[a[i] - min]++;
}
int index = 0;
//遍历范围
for (int i = 0; i < range; i++)
{
while (counta[i]--)
{
a[index] = i + min;
index++;
}
}
}