算法分类
十种常见排序算法可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
算法复杂度
相关概念
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机
内执行时所需存储空间的度量,它也是数据规模n的函数。
1、冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.1算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
1.2动图演示
1.3代码实现
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
const int maxn = 100;
int arr[maxn], length;
//设置随机数组
void setRandArray(int a[]) {
srand((unsigned)time(NULL));
for (int i = 0; i < length; i++) {
int j = rand() % 100;
a[i] = j;
}
}
//输出数组
void showArray(int* a) {
cout << "数组元素内容:";
for (int i = 0; i < length; i++) {
cout << a[i] << " ";
}
cout << endl;
}
//冒泡排序
void bubbleSort(int *arr) {
for (int i = 0; i < length - 1; i++) {
for (int j = 0; j < length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
int main()
{
cout << "输入元素个数:";
cin >> length;
setRandArray(arr);
showArray(arr);
bubbleSort(arr);
cout << "冒泡排序后的数组:" << endl;
showArray(arr);
return 0;
}
2、选择排序(Selection Sort)
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
2.1 算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1..n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
2.2 动图演示
2.3 代码实现
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
const int maxn = 100;
int arr[maxn], length;
//设置随机数组
void setRandArray(int a[]) {
srand((unsigned)time(NULL));
for (int i = 0; i < length; i++) {
int j = rand() % 100;
a[i] = j;
}
}
//输出数组
void showArray(int* a) {
cout << "数组元素内容:";
for (int i = 0; i < length; i++) {
cout << a[i] << " ";
}
cout << endl;
}
//选择排序
void selectionSort(int arr[]) {
int minIndex, temp;
for (int i = 0; i < length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
int main()
{
cout << "输入元素个数:";
cin >> length;
setRandArray(arr);
showArray(arr);
selectionSort(arr);
showArray(arr);
return 0;
}
3、插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
3.1 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
3.2 动图演示
3.2 代码实现
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
const int maxn = 100;
//设置随机数组
void setRandArray(int a[], int length) {
srand((unsigned)time(NULL));
for (int i = 0; i < length; i++) {
int j = rand() % 100;
a[i] = j;
}
}
//输出数组
void showArray(int* a, int length) {
cout << "数组元素内容:";
for (int i = 0; i < length; i++) {
cout << a[i] << " ";
}
cout << endl;
}
//插入排序
void insertionSort(int *arr,int len){
int preIndex,current;
for(int i = 1; i < len; i++){
preIndex = i - 1;
current = arr[i];
while(preIndex >= 0 && arr[preIndex] > current){
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
}
int main()
{
int arr[maxn], length;
cout << "数组元素个数:";
cin >> length;
setRandArray(arr,length);
showArray(arr,length);
insertionSort(arr,length);
showArray(arr,length);
return 0;
}
4、希尔排序(Shell Sort)
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
4.1 算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
首先它把较大的数据集合分割成若干个小组(逻辑上分组),然后对每一个小组分别进行插入排序,此时,插入排序所作用的数据量比较小(每一个小组),插入的效率比较高
可以看出,他是按下标相隔距离为4分的组,也就是说把下标相差4的分到一组,比如这个例子中a[0]与a[4]是一组、a[1]与a[5]是一组...,这里的差值(距离)被称为增量
每个分组进行插入排序后,各个分组就变成了有序的了(整体不一定有序)
此时,整个数组变的部分有序了(有序程度可能不是很高)
然后缩小增量为上个增量的一半:2,继续划分分组,此时,每个分组元素个数多了,但是,数组变的部分有序了,插入排序效率同样比高
同理对每个分组进行排序(插入排序),使其每个分组各自有序
最后设置增量为上一个增量的一半:1,则整个数组被分为一组,此时,整个数组已经接近有序了,插入排序效率高
同理,对这仅有的一组数据进行排序,排序完成
4.2 动图演示
4.3 代码实现
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
const int maxn = 100;
//设置随机数组
void setRandArray(int a[], int length) {
srand((unsigned)time(NULL));
for (int i = 0; i < length; i++) {
int j = rand() % 100;
a[i] = j;
}
}
//输出数组
void showArray(int* a, int length) {
cout << "数组元素内容:";
for (int i = 0; i < length; i++) {
cout << a[i] << " ";
}
cout << endl;
}
void insertI(int arr[], int gap, int i){
int inserted = arr[i];
int j;
//插入的时候按组进行插入(组内元素两两相隔gap)
for(j = i - gap; j >= 0 && inserted < arr[j]; j -= gap){
arr[j + gap] = arr[j];
}
arr[j + gap] = inserted;
}
void shellSort(int arr[], int len){
//进行分组,最开始时的增量(gap)为数组长度的一半
for(int gap = len / 2; gap > 0; gap /= 2){
//对各个分组进行插入排序
for(int i = gap; i < len; i++){
//将arr[i]插入到所在分组的正确位置上
insertI(arr,gap,i);
}
}
}
int main()
{
int arr[maxn], length;
cout << "数组元素个数:";
cin >> length;
setRandArray(arr,length);
showArray(arr,length);
shellSort(arr,length);
showArray(arr,length);
return 0;
}
function shellSort(arr) {
var len = arr.length;
for (var gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
// 注意:这里和动图演示的不一样,动图是分组执行,实际操作是多个分组交替执行
for (var i = gap; i < len; i++) {
var j = i;
var current = arr[i];
while (j - gap >= 0 && current < arr[j - gap]) {
arr[j] = arr[j - gap];
j = j - gap;
}
arr[j] = current;
}
}
return arr;
}
5、归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
归并排序的主要思想是分治法。主要过程是:
- 将n个元素从中间切开,分成两部分。(左边可能比右边多1个数)
- 将步骤1分成的两部分,再分别进行递归分解。直到所有部分的元素个数都为1。
- 从最底层开始逐步合并两个排好序的数列。
考虑一个问题,如何将两个有序数列合并成一个有序数列?
很简单,由于两个数列都已经有序,我们只需从两个数列的低位轮番拿出各自最小的数来PK就就行了,输的一方为小值,将这个值放入临时数列,然后输的一方继续拿出一个值来PK,直至有一方没有元素后,将另一方的所有元素依次接在临时数列后面即可。此时,临时数列为两个数列的有序合并。归并排序中的归并就是利用这种思想。
5.1 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
5.2 动图演示
5.3 代码实现
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
const int maxn = 100;
//随机数组
int arr[maxn];
int length;
//设置随机数组
void setRandArray(int a[]) {
srand((unsigned)time(NULL));
for (int i = 0; i < length; i++) {
int j = rand() % 100;
a[i] = j;
}
}
//输出数组
void showArray(int *a) {
cout << "数组元素内容:";
for (int i = 0; i < length; i++) {
cout << a[i] << " ";
}
cout << endl;
}
//将数组a的[L1,R1]与[L2,R2]区间合并为有序区间,此处,L2即为L1 + 1
void merge(int a[], int L1, int R1, int L2, int R2) {
int i = L1; //i指向a[L1]
int j = L2; //j指向a[L2]
int temp[maxn]; //存放合并后的数组
int index = 0; //index为temp的下标
while (i <= R1 && j <= R2) {
if (a[i] <= a[j]) {
temp[index++] = a[i++];
}
else {
temp[index++] = a[j++];
}
}
while (i <= R1)
temp[index++] = a[i++];
while (j <= R2)
temp[index++] = a[j++];
//合并后,再赋回数组a
for (int i = 0; i < index; i++) {
a[L1 + i] = temp[i];
}
}
void mergeSort(int arr[], int left, int right) {
if (left < right) { //只要left小于right
int mid = (left + right) / 2; //取left和right中点
mergeSort(arr, left, mid); //递归,左区间归并
mergeSort(arr, mid + 1, right); //递归,右区间归并
merge(arr, left, mid, mid + 1, right); //左右区间合并
}
}
int main() {
cout << "输入数组元素个数:";
cin >> length;
setRandArray(arr);
showArray(arr);
mergeSort(arr, 0, length - 1);
cout << "归并排序后:" << endl;
showArray(arr);
return 0;
}
5.4 算法分析
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。