文章目录
- 前言
- 一、冒泡排序
- 二、选择排序
- 三、插入排序
- 四、希尔排序
- 五、堆排序
- 六、快速排序
- 快排的递归方式
- 快排的非递归方式
- 七、归并排序
- 自上而下的递归
- 自下而上的迭代
- 总结
前言
排序: 所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性: 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序: 数据元素全部放在内存中的排序。
外部排序: 数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
常见的内部排序算法有:
一、冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
1.算法步骤
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 //这是内层循环所控制的
3.针对所有的元素重复以上的步骤,除了最后一个。 //这是外层循环所控制的
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
2.动画演示
3.效率分析
当数据全是正序时,遍历一次即可,通过flag直接跳出外层循环,此时速度最快。
当数据全是反序时,每次都需要遍历和交换,此时速度最慢。
4.代码设计
//冒泡排序
template<class T>
void bubble_sort(std::vector<T>& arr, int size)
{
for (int i = 0; i < size; i++)
{
int flag = 0;
for (int j = 0; j < size - 1 - i; j++)
{
if (arr[j + 1] < arr[j]) // <是为了保证稳定性
{
swap(arr[j + 1], arr[j]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
二、选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
1.算法步骤
1.首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2.再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3.重复第二步,直到所有元素均排序完毕
2.动画演示
3.代码设计
//选择排序
template<class T>
void select_sort(std::vector<T>& arr, int size)
{
for (int i = 0; i < size ; i++)
{
int min = i;
for (int j = i + 1; j < size ; j++)
{
if (arr[min] > arr[j]) // >是为了保证稳定性
min = j;
}
if (min != i) swap(arr[min], arr[i]);
}
}
三、插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
1.算法步骤
1.将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2.从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面,保证稳定性。)
2.动画演示
3.代码设计
//插入排序
template<class T>
void insert_sort(std::vector<T>& arr, int size)
{
for (int i = 1; i < size; i++)
{
int key = arr[i];
int end = i - 1;
while (end >= 0 && key < arr[end]) // <是为了保证稳定性
{
arr[end +1] = arr[end];
end--;
}
arr[end + 1] = key;
}
}
四、希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
1.算法步骤
1.选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
2.按增量序列个数 k,对序列进行 k 趟排序;
3.每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
2.过程演示
3.算法分析
希尔排序时间复杂度是 O(n(1.3-2)),空间复杂度为常数阶 O(1)。希尔排序没有时间复杂度为 O(n(logn)) 的快速排序算法快 ,因此对在这里插入代码片
中等大小规模表现良好,但对规模非常大的数据排序不是最优选择,总之比一般 O(n2 ) 复杂度的算法快得多。
步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。
4.代码设计
//希尔排序
template<class T>
void shell_sort(std::vector<T>& arr, int size)
{
int gap = size;
while (gap > 1)
{
gap /= 2;
for (int i = 0; i < size - gap; i++)
{
int end = i;
int key = arr[end + gap];
while (end >= 0 && key < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
arr[end + gap] = key;
}
}
}
五、堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。
1.算法步骤
1.将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆;
2.把堆首(最大值)和堆尾互换;
3.把堆的尺寸缩小 1,并调用 AdjustDown(0),目的是把新的数组顶端数据调整到相应位置;
4.重复步骤 2,直到堆的尺寸为 1。
2.动画演示
3.代码设计
//堆排序
template<class T>
void AdjustDown(std::vector<T>& arr, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
int max = child;
if (child + 1 < size && arr[child + 1] > arr[child])
max = child + 1;
if (arr[parent] < arr[max])
{
swap(arr[max], arr[parent]);
parent = max;
child = parent * 2 + 1;
}
else
break;
}
}
template<class T>
void heap_sort(std::vector<T>& arr, int size)
{
for (int i = (size - 1 - 1) / 2; i >= 0; i--)
AdjustDown(arr, size, i);
for (int i = 1; i < size; i++)
{
swap(arr[0], arr[size - i]);
AdjustDown(arr, size - i, 0);
}
}
六、快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n ogn) 的排序算法表现要更好:
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
快排的递归方式
1.算法步骤
1.从数列中挑出一个元素,称为 “基准”(pivot);
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
2.动画演示
3.代码设计
//快速排序的优化——三数取中(key)
template<class T>
int getKey(std::vector<T>& arr, int begin, int end)
{
int mid = (begin + end) / 2;
if (arr[begin] < arr[mid])
{
if (arr[mid] < arr[end])
return mid;
else if (arr[begin] > arr[end])
return begin;
else
return end;
}
else
{
if (arr[mid] > arr[end])
return mid;
else if (arr[end] > begin)
return begin;
else
return end;
}
}
//快速排序
template<class T>
int partition1(std::vector<T>& arr, int left, int right)
{
int mid = getKey(arr, left, right);
swap(arr[left], arr[mid]);
//left会移动,利用key保存最左边的下标;
int key = left;
while (left < right)
{
while (left < right && arr[right] >= arr[key])//往左找小
--right;
while (left < right && arr[left] <= arr[key]) //往右找大
++left;
if (left < right)
swap(arr[left], arr[right]);
}
//从右边开始找可以确定相遇位置的值要小于key
swap(arr[left], arr[key]);
return left;
}
template<class T>
void quick_sort(std::vector<T>& arr, int begin, int end)
{
if (begin >= end) return;
int mid = partition1(arr, begin, end);
quick_sort(arr, begin, mid - 1);
quick_sort(arr, mid + 1, end);
}
快排的非递归方式
由于递归方式会大大增加函数栈帧的层数,导致栈溢出,所以利用栈去模拟递归的思想实现:
template<class T>
void quick_sortNoR(std::vector<T>& arr, int left, int right)
{
std::stack<T> s;
s.push(left);
s.push(right);
while (!s.empty())
{
right = s.top();
s.pop();
left = s.top();
s.pop();
int div = partition1(arr, left, right);
if (div + 1 < right)
{
s.push(div + 1);
s.push(right);
}
if (left < div - 1)
{
s.push(left);
s.push(div - 1);
}
}
}
七、归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归;
- 自下而上的迭代;
1.算法步骤
1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
2.设定两个指针,最初位置分别为两个已经排序序列的起始位置;
3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
4.重复步骤 3 直到某一指针达到序列尾;
5.将另一序列剩下的所有元素直接复制到合并序列尾。
2.动画演示
3.代码设计
自上而下的递归
//归并排序 对于[)区间
template<class T>
void merge(std::vector<T>& arr, int l, int mid, int r) {
int index = 0;
int ptrL = l;
int ptrR = mid;
static std::vector<T>tempary;
if (arr.size() > tempary.size()) {
tempary.resize(arr.size());
}
while (ptrL < mid && ptrR < r) {
if (arr[ptrL] < arr[ptrR])
tempary[index++] = arr[ptrL++];
else
tempary[index++] = arr[ptrR++];
}
while (ptrL < mid) {
tempary[index++] = arr[ptrL++];
}
while (ptrR < r) {
tempary[index++] = arr[ptrR++];
}
std::copy(tempary.begin(), tempary.begin() + index , arr.begin() + l);
}
template<class T>
void merge_sort(std::vector<T>& arr, int l, int r) { // sort the range [l, r) in arr
if (r - l <= 1) return;
int mid = (l + r) / 2;
merge_sort(arr, l, mid);
merge_sort(arr, mid, r);
merge(arr, l, mid, r);
}
自下而上的迭代
template<class T>
void merge_sortNoR(std::vector<T>& arr, int sz)
{
static std::vector<T> tempary;
if (tempary.size() < arr.size())
tempary.resize(arr.size());
int gap = 1;
while (gap < arr.size())
{
for (int i = 0; i < sz; i += 2 * gap)
{
int b1 = i, e1 = i + gap - 1;
int b2 = i + gap, e2 = i + 2 * gap - 1;
if (e1 >= sz) break;//第一组越界
if (b2 >= sz) break;//第二组全部越界
if (e2 >= sz) e2 = sz - 1;//第二组部分越界,继续归并
int index = i;
while (b1 <= e1 && b2 <= e2)
{
if (arr[b1] <= arr[b2])
tempary[index++] = arr[b1++];
else
tempary[index++] = arr[b2++];
}
while (b1 <= e1)
{
tempary[index++] = arr[b1++];
}
while (b2 <= e2)
{
tempary[index++] = arr[b2++];
}
std::copy(tempary.begin(), tempary.begin() + index, arr.begin());
}
gap *= 2;
}
}
总结
1.选择排序不稳定的原因:
比如 在序列5,3,5,2中,2 是最小值,会和第 1 个 5 进行交换,那第 1 个 5 就去了第 2 个 5 的后面,两个 5 的相对位置发生改变。
2.希尔排序不稳定的原因:
相同元素在不同子序列中可能被分到不同的位置,这导致了相同元素的相对位置可能被改变。
3.堆排序不稳定的原因:
在建立堆的调整步骤里,由于关键字相同的两个记录位置并不会被调换,所以建堆的时候是稳定的。但是,在堆顶与堆尾交换的时候两个相等的记录在序列中的相对位置就可能发生改变。
4.快速排序不稳定的原因:
比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。