这里介绍的都是内部排序,即在所有数据都在内存中的,还有这些排序都是通过比较来获得次序信息,因此这些排序算法都称为比较排序。
最简单的有冒泡排序、简单选择排序、直接插入排序,希尔排序是对插入排序的优化,堆排序是在堆这种数据结构上的选择排序,而快速排序和归并排序都利用了分治的思想,利用递归来降低时间复杂度。
非比较排序常见有:计数排序,基数排序,桶排序;外部排序常见有多路归并排序,有兴趣的可以通过搜索引擎了解一下。
以下为常见7种排序算法的时间复杂度,空间复杂度以及稳定性等信息。
排序的时候可能会用到交换,以下为两种不用临时变量的swap
方法:
void swap(int& a, int& b)
{
if (a != b)
{
a ^= b;
b ^= a;
a ^= b;
}
}
void swap2(int& a, int& b)
{
if (a != b)
{
a = a + b;
b = a - b;
a = a - b;
}
}
从左到右,陆续比较相邻的两个数,如果左边的数比右边的大,则交换位置,让最大的数冒泡到最右边。每次冒泡一个最大的数,进行n-1次。最后实现从左到右,从小到大顺序。
void bubbleSort(int array[], int length)
{
for (int i = 0; i < length-1; i++)
{
//优化1:设置标志位,如果一次遍历没有发生一次交换,
//说明已经排好序,则退出
bool flag = false;
for (int j = 0; j < length-i-1; j++)
{
if (array[j] > array[j+1])
{
swap(array[j],array[j+1]);
flag = true;
}
}
if (false == flag)
break;
}
}
//优化2:记录一轮下来标记的最后位置,下次从头部遍历到这个位置就行了
//因为后面没有发生交换说明后面已经排序好了
void bubbleSort2(int array[], int length)
{
int flag = length;
while(flag > 0)
{
int end = flag;
flag = 0;
for (int j = 0; j < end-1; j++)
{
if (array[j] > array[j+1])
{
swap(array[j],array[j+1]);
flag = j+1;
}
}
}
}
从左到右,通过n-i次比较,记录下从i到最后最小的值的位置,然后把它交换到i位置,i从0到n-1。每次都会把最小的交换到最左边,最终达到顺序状态。
void selectSort(int array[], int length)
{
for (int i = 0; i < length - 1; i++)
{
int minIndex = i;
for (int j = i + 1; j < length; j++)
{
if (array[j] < array[minIndex])
minIndex = j;
}
if ( minIndex != i )
swap(array[i], array[minIndex]);
}
}
从左到右,将一个个插入到前面排序好的数组中。如果插入到数组中间,需要所有大于插入的数往后挪一位。
void insertSort(int array[], int length)
{
int i, j, temp;
for (i = 1; i < length; i++)
{
temp = array[i];
for (j = i; j > 0 && array[j-1] > temp; j--)
array[j] = array[j-1];
array[j] = temp;
}
}
希尔排序是插入排序的一种高效率实现,也称递减增量排序,主要利用了在序列基本有序时插入排序会更加高效的特点。
基本思想:先将待排序序列按一定步长分割成若干个子序列分别进行直接插入排序,逐渐减少步长,步长为1时就是普通的一次直接插入排序。
void shellSort(int array[], int length)
{
int i, j, inc, temp;
//步长递减的方式可以改变
for (inc = length/2; inc > 0; inc /= 2)
{
for (i = inc; i < length; i++)
{
temp = array[i];
for (j = i; j >= inc; j -= inc)
{
if (array[j - inc] > temp)
array[j] = array[j - inc];
else
break;
}
array[j] = temp;
}
}
}
上述例子使用n/2为步长,实际使用可以根据序列特点改变,来增加效率。
归并排序的思想就是分治-归并,将序列分成两半分别递归排序,再将有序的两个序列归并在一起。
void merge(int array[], int temp[], int left, int mid, int right)
{
int i = left;
int j = mid + 1;
int k = left;
while(i <= mid && j <= right)
{
if (array[i] > array[j])
temp[k++] = array[j++];
else
temp[k++] = array[i++];
}
while (i <= mid)
temp[k++] = array[i++];
while (j <= right)
temp[k++] = array[j++];
for(i = 0; i <=right; i++)
array[i] = temp[i];
}
void mSort(int array[], int temp[], int left, int right)
{
if (left < right)
{
int mid = left + (right - left) / 2;
mSort(array, temp, left, mid);
mSort(array, temp, mid + 1, right);
merge(array, temp, left, mid, right);
}
}
void mergeSort(int array[], int length)
{
int *temp = new int[length];
mSort(array, temp, 0, length - 1);
delete []temp;
}
快速排序也是利用分治思想,不过和归并排序不同,它没有归并的过程,而是递归的将区间内大于基准的和小于基准的分别放在基准数左右,直至各区间只有一个数。
基本步骤:
1. 从序列中挑出一个元素作为基准数;
2. 分区过程,将比基准数大的全部放到右边,小于或等于基准数的全部放到左边;
3. 再对左右区间递归重复上述步骤,直到各区间只有一个数。
//左右挖坑法: 拿出第一个元素作为基准数,即挖第一个坑,然后从后面找一个比它小的填坑,然后又形成一个坑
//再从前面找一个比基准大的数填坑,然后又会形成一个坑。如此循环,最后一个坑填入基准即可。
void qSort(int array[], int left, int right)
{
if (left >= right)
return;
int i = left;
int j = right;
int pivot = array[left]; //基准
while (i < j)
{
while (i < j && array[j] > pivot)
j--;
if (i < j)
array[i++] = array[j];
while (i < j && array[i] < pivot)
i++;
if (i < j)
array[j--] = array[i];
}
array[i] = pivot; //基准填回
qSort(array, left, i - 1);
qSort(array, i + 1, right);
}
void quickSort(int array[], int length)
{
qSort(array, 0, length - 1);
}
参考链接
堆排序是借助二叉堆来实现的选择排序,思想同简单选择排序,如果想从左到右升序排序就要用最大堆。
二叉堆(以最大堆为例)是一颗完全二叉树,并满足以下两个特性:
一般是用数组来表示二叉堆,并有以下性质:
每次插入都是放在数组最后,新数据的父节点到根节点必然是一个有序的序列,用类似于直接插入排序的方式将这个数据插入有序区间即可。
//从下到上调整最大堆
void fixupMaxHeap(int a[], int i)
{
int parent, temp;
temp = a[i];//最后一个插入的数
parent = (i-1)/2; //父节点
while (parent >=0 && i > 0)
{
if (a[parent] >= temp)
break;
a[i] = a[parent]; //把较小的数往下移动,替换它的子节点
i = parent;
parent = (i - 1) / 2;
}
a[i] = temp;
}
//往最大堆添加新数据
void addNumToMaxHeap(int a[], int n, int num)
{
a[n] = num;
fixupMaxHeap(a, n);
}
堆每次只能删除第0个数据,删除后需要重新调整剩余元素使其仍为最大堆。为了便于重建堆,实际操作是将最后一个值赋给根节点,删除最后一个值,然后再从根节点开始一次从上到下的调整。
调整时先将左右子节点中最大的与父节点比较,如果父节点比这个大的子节点还大就说明不需要调整了,否则交换父节点和最大子节点的位置,然后再对调整后的那个节点继续之前的操作。
//从上到下调整最大堆
void fixdownMaxHeap(int a[], int start, int length)
{
int i = start;
int j = 2 * i + 1; //左子节点
int temp = a[start]; //替换后的根节点
while (j < length)
{
if (j + 1 < length && a[j + 1] > a[j]) //在左右孩子中找最大的
j++;
if (a[j] <= temp)
break;
a[i] = a[j]; //把较大的子节点往上移动,替换它的父节点
i = j;
j = i * 2 + 1;
}
a[i] = temp;
}
//在最大堆中删除数
void deleteNumFromMaxHeap(int a[], int length)
{
swap(a[0], a[length-1]); //交换最后一个数和根节点
fixdownMaxHeap(a, 0, length-1);
}
所谓堆化数组就是把一个数组调整为堆的结构。堆化数组不需要一个个从数组中取出数据来建立,因为所有叶子节点只有一个点,已经是一个堆了。
void buildMaxHeap(int a[], int length)
{
for (int i = (length / 2) - 1; i >=0; i--)
fixdownMaxHeap(a, i, length);
}
最后在建好的堆上,第0个数据就是最大的数据,取出这个数据再执行下堆的删除操作,反复进行就可以得到一个排好序的序列了。
//调整最大堆
void adjustMaxHeap(int a[], int start, int length)
{
int i = start;
int j = 2 * i + 1; //左子节点
int temp = a[start]; //替换后的根节点
while (j < length)
{
if (j + 1 < length && a[j + 1] > a[j]) //在左右孩子中找最大的
j++;
if (a[j] <= temp)
break;
a[i] = a[j]; //把较大的子节点往上移动,替换它的父节点
i = j;
j = i * 2 + 1;
}
a[i] = temp;
}
void heapSort(int a[], int length)
{
int i;
//创建堆
for (i = (length / 2) - 1; i >=0; i--)
adjustMaxHeap(a, i, length);
//删除堆根节点到数组最后
for (i = length - 1; i > 0; i--)
{
swap(a[i], a[0]);
adjustMaxHeap(a, 0, i);
}
}
经典排序算法总结与实现
常用排序算法总结(一)
常见排序算法 - 堆排序 (Heap Sort)
白话经典算法系列之七 堆与堆排序