0. 排序算法分析角度
a. 算法的执行效率
- 最好情况、最坏情况、平均时间复杂度
- 时间复杂度的系数、常数、低阶
- 比较次数、交换或移动次数
b. 算法的内存消耗
空间复杂度,当空间复杂度为O(1)时,被称为原地排序
c. 算法的稳定性
如果待排序的序列存在值相等的元素,经过排序之后,相等元素之间的先后顺序保持不变。
1. 冒泡排序
冒泡排序只会操作相邻的两个数据。每次操作都会对两个相邻的元素进行比较,如果不满足比较条件,便进行交换,直到有序。每次冒泡排序至少会让一个元素到最终位置。
可以通过一个标志位,当没有数据交换时,表示数据已经有序,设置标记,来进行优化。
//data为排序数组,n为数组长长度,按照从小到大排序。
//通过flag来优化排序,若没有交换说明已经有序,直接结束循环。
void bubble_sort(int data[], int n)
{
if (n < 2)
return;
for (int i = 0; i < n - 1 ; i++)
{
bool flag = false;
for (int j = 0; j < n - i - 1; j++)
{
//相邻元素进行比较,前面大于后面元素时,交换元素,并标识交换
if (data[j] > data[j+1])
{
flag = true;
swap(data[j], data[j + 1]);
}
}
//已经有序,则退出
if (!flag)
break;
}
}
2. 插入排序
插入排序通过将数据分为已排序区间和未排序区间。初始已排序区间只有1个元素,即为数组中的第一个元素。核心思想是将未排序区间中的元素,逐个在已排序区间找到自己的位置,插入其中,保证已排序区间一直处于有序。直到未排序区间为空。
//data为排序数组,n为数组长长度,按照从小到大排序。
//从未排序区间选择元素插入到已排序区间,[0,i)为已排序区间,[i,n)为未排序区间。
void insert_sort(int data[], int n)
{
if (n < 2)
return;
//从未排序区间选择元素插入到已排序区间,[0,i)为已排序区间,[i,n)为未排序区间
int value = 0;
for (int i = 1; i < n; i++)
{
value = data[i];
int j = i - 1;
for (; j >= 0; j--)
{
//当待插入元素小于前面数据,移动数据
if (value < data[j])
{
data[j + 1] = data[j];
}
//当不用交换时,退出循环
else
break;
}
//插入value于已排序区间
data[j + 1] = value;
}
}
3. 选择排序
选择排序思想类似插入排序,也分为已排序区间和未排序区间。核心思想为从未排序区间中寻找到最小元素,插入到已排序区间末尾。当未排序区间为空时,排序完成。
//data为排序数组,n为数组长长度,按照从小到大排序。
//从未排序区间选择最小的元素插入到已排序区间末尾,[0,i)为已排序区间,[i,n)为未排序区间。
void selection_sort(int data[], int n)
{
if (n < 2)
return;
//从未排序区间找出最小元素插入到已排序区间,[0,i)为已排序区间,[i,n)为未排序区间
int min_index;
for (int i = 0; i < n - 1; i++)
{
//找到[i,n)区间中最小的元素
min_index = i;
for (int j = i + 1; j < n; j++)
{
if (data[min_index] > data[j])
{
min_index = j;
}
}
//将最小元素插入到已排序区间末尾
swap(data[i], data[min_index]);
}
}
4. 归并排序
归并排序的核心思想是将一个待排序数组,分为前后两部分,然后对左右两部分分别进行排序,最后再合并在一起,得到一个有序数组。是一种分之思想,可以采用递归的编程方式来实现。
//合并递归后的左右区间,[left,mid],[mid+1,right]
void merge(int data[], int left, int mid, int right)
{
//临时数组,用来合并
int len = right - left + 1;
int* tmp = new int[len];
//i为左区间index,j为右区间index,k为tmp数组index
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right)
{
//左区间<右区间首元素,加入data[left]到tmp数组
if (data[i] < data[j])
tmp[k++] = data[i++];
//反之,加入data[mid+1] 到tmp 数组
else
tmp[k++] = data[j++];
}
//若左区间剩余为空,将右区间剩余加入tmp数组
if (i > mid)
{
while (j <= right)
tmp[k++] = data[j++];
}
//若右区间剩余为空,将左区间剩余加入tmp数组
else
{
while (i <= mid)
tmp[k++] = data[i++];
}
//将tmp数组copy到data[left,right]
for (int i = 0; i < len; i++)
{
data[i + left] = tmp[i];
}
delete[] tmp;
}
//归并排序内部递归部分,[left,right]区间
void __merge_sort(int data[], int left, int right)
{
//递归终止条件
if (left >= right)
return;
int mid = left + ((right - left) >> 2);
__merge_sort(data, left, mid);
__merge_sort(data, mid + 1, right);
merge(data, left, mid, right);
}
//data为待排序数组,n为数组大小,采用从小到大方式排序
void merge_sort(int data[], int n)
{
__merge_sort(data, 0, n - 1);
}
5. 快速排序
快速排序与归并排序类似,但不同的是:快速排序通过找到pivot枢点来控制左右区间的划分;同时,归并排序处理问题是先划分区间,再处理,快排是通过先处理,再划分。
快排可以通过调整partition分区函数来使时间复杂度更稳定与O(nlogn),例如三数取中,随机数等方法。是非稳定排序,但由于空间复杂度为O(1),源码库中排序底层也都运用了快排,或与其他排序相结合。
//分区函数,采用取最后一个元素为pivot,
//返回分区结点的下标
int partition(int data[], int left, int right)
{
int pivot = data[right];
//[left,r)为小于pivot部分,(r,right]为大于pivot部分
int r = left;
for (int i = left; i < right; i++)
{
//小于pivot加入到[left,r)最后
if (data[i] <= pivot)
{
swap(data[i], data[r]);
++r;
}
}
swap(data[r], data[right]);
return r;
}
//快速排序内部递归部分,[left,right]区间
void __quick_sort(int data[], int left, int right)
{
if (left >= right)
return;
//对原区间进行分区
int pos = partition(data, left, right);
__quick_sort(data, left, pos - 1);
__quick_sort(data, pos + 1, right);
}
//data为待排序数组,n为数组大小
void quick_sort(int data[], int n)
{
__quick_sort(data, 0, n - 1);
}
6. 桶排序
桶排序的核心思想是通过将数据先分到几个有序的桶里,其中这些桶天然有序(例如可以将0 ~ 99分为10个桶,0 ~ 9, 10 ~ 19,20 ~ 29...) ,然后再对每个桶进行排序。排序完成之后,将各个桶的数据依次组合,就能得到有序的序列。
核心部分是能分为有序的桶,要求每个桶的数据尽量均匀,适合用于外部排序中。
7. 计数排序
可以认为计数排序是桶排序的一种特殊情况,当每个桶的粒度大小为1时,遍历一遍数据,加入桶中,依次遍历即为有序序列。
计数排序只能用于数据范围不大的场合,对一些特殊的数字要进行转换,转换为非负整数。
8. 基数排序
基数排序通过将数据通过按位来排序,例如11位的手机号码,可以先根据第一位手机号排序,再根据第二位,直到第11位,可以得到一个有序的序列。中间可以采用桶排序、计数排序等其他排序。
每一位的数据范围也不能很大。
9. 排序总结
排序算法 | 最差情况时间复杂度 | 最好情况时间复杂度 | 平均时间复杂度 | 空间时间复杂度(原地排序?) | 稳定性 | 使用场景 |
---|---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定排序 | 一般不采用 |
插入排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定排序 | 源码中数据量小时,采用 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 非稳定排序 | 一般不采用 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定排序 | 由于使用额外空间较多,一般选用快排O(1) |
快速排序 | O(n2) | O(nlogn) | O(nlogn) | O(1) | 非稳定排序 | 源码中数据量大时,采用 |
桶排序 | - | - | O(n) | O(n) | 稳定排序 | 不是基于比较,适用于外部排序 |
计数排序 | - | - | O(n+k) k为数据范围 | O(n) | 稳定排序 | - |
基数排序 | - | - | O(d*n) d为分配的维度 | - | 稳定排序 | - |