- 冒泡排序
- 选择排序
- 堆排序
- 插入排序
- 希尔排序
- 归并排序
- 快速排序
- 排序算法的稳定性
冒泡排序
冒泡排序:
一个要排序的数列,将其遍历若干次,每次遍历时都将相邻的两个元素进行比较,将较大/较小的元素放在进行比较两元素后者的位置,这样一次遍历结束后,最大/最小的元素就放在了数列的最后位置。第二次遍历后,第二大/小的元素放在数组倒数第二个位置,重复多次操作做,便可得到一个有序数列。
代码实现
方案一:
void Bubble_Sort(int arr[], int n)
{
int i = 0;
int j = 0;
for (; i < n; i++)
{
for (j = 0; j < n-1-i; j++)
{
if (arr[j] < arr[j+1])
{
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
方案二:
void Bubble_Sort(int arr[], int n)
{
int i = 0;
int j = 0;
int flag = 0;
for (; i < n; i++)
{
for (j = 0; j < n-1-i; j++)
{
if (arr[j] < arr[j+1])
{
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
flag++;
}
}
if (flag == 0)
{
break; // 数组未发生交换
}
}
}
时间复杂度
O(N^2)(最坏情况),最好情况时间复杂度为O(N)
空间复杂度
O(1)
选择排序
选择排序:
选择排序在每一次遍历数组时,找出数组元素中数值最小/最大的元素放置数组前端,直到所有待排序的数组有序
代码实现:
void Select_Sort(int arr[], int n)
{
int i = 0;
int j = 0;
int min = 0;
for (; i < n; i++)
{
min = i;
for (j = i+1; j < n; j++)
{
if (arr[j] < arr[min])
{
min = j;
}
}
if (min != i)
{
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
}
时间复杂度
假设数组有N个数,循环内比较次数为N-1,N-2,N-3…,1,得到一个等差数列,求和后得
(N-1+-1)*N/2
时间复杂度为O(n^2)
空间复杂度
O(1)
选择排序与冒泡排序比较:
1.两排序方式的时间复杂度相同,但在交换时,选择排序最多交换N-1次交换,而冒泡排序最多发生N^2次,因此,选择排序性能上优于冒泡排序
2.选择排序的思想比冒泡排序更易于理解
堆排序
堆
具有以下性质的完全二叉树:
1.每个结点的值都大于等于其左右子节点的值,称为大顶堆
2.每个结点的值都小于等于其左右子节点的值,称为小顶堆
堆排序:
将待排序序列构造成一个大顶堆/小顶堆,根节点就是该序列里面最大/最小的元素,将其与末尾元素进行交换,此时末尾元素即为该序列的最大/最小元素。
将剩余元素重新构造成一个大顶堆/小顶堆,重复执行上面的过程,得到最终的有序序列
代码实现:
void AdjustDown(int arr[], int index, int n)
{
int parent = index;
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && arr[child] > arr[child+1])
{
child++;
}
if (arr[parent] > arr[child] )
{
int tmp = arr[parent];
arr[parent] = arr[child];
arr[child] = tmp;
}
else
{
break;
}
parent = child;
child = parent * 2 + 1;
}
}
void HeapSort(int arr[], int n)
{
//将数组转化为堆
//最后一个非叶子节点
int index = n / 2 - 1;
while (index)
{
AdjustDown(arr, index--, n);
}
AdjustDown(arr, index, n);
while (n > 1)
{
//交换末尾与根结点, 降序,小顶堆
int tmp = arr[n-1];
arr[n-1] = arr[0];
arr[0] = tmp;
n--;
AdjustDown(arr, index, n);
}
}
时间复杂度:
初始化建堆的时间复杂度为O(n),排序重建堆的时间复杂度为nlog(n),总的时间复杂度为=O(nlogn)
空间复杂度:
O(1)
插入排序
插入排序:
插入排序是基于比较的排序,插入排序每一步都将一个待排数据插入到已经排号的数据序列中
代码实现:
void Insert_Sort(int arr[], int n)
{
int i = 0;
int j = 0;
for (i = 1; i < n; i++)
{
if (arr[i-1] > arr[i])
{
int tmp = arr[i];
j = i;
while (j > 0 && arr[j-1] > tmp)
{
arr[j] = arr[j-1];
//后移一位留出空位插入
j--;
}
arr[j] = tmp;
}
}
}
时间复杂度:
比较次数为1,2,3,…, N-1,N,时间复杂度为O(N^2)(最坏情况)
O(N)(最好情况,数组有序)
空间复杂度:
O(1)
希尔排序
希尔排序:
也称缩小增量排序,是直接插入排序的一种改进版本,通过记录下标的一定增量进行分组(增量没有特别好的选择,希尔建议的增量是长度的1/2,每次增量的缩小也是1/2),按比较的大小交换分组内的数据,缩小增量改变分组后重复过程,得到有序序列
代码实现:
void Shell_Sort(int arr[], size_t len)
{
assert(arr);
size_t gap = len; //增量
size_t i = 0;
while (gap > 1)
{
gap = gap/2;
for (i = gap; i < len; i++)
{
int key = arr[i];
int j = i - gap;
while (j >= 0 && key < arr[j])
{
int tmp = arr[j];
arr[j] = arr[j+gap];
arr[j+gap] = tmp;
j = j-gap;
}
}
}
}
时间复杂度:
最好情况为O(N),最坏情况为O(N^2),平均情况为O(N^1.3)
空间复杂度:
O(1)
归并排序
归并排序:
通过分治法的思想,将一个序列不断平均分为两个序列,直到再也没办法分割时,每个序列分别有序,然后依次将序列有序合并,直到合并完成
代码实现:
void _Merge_Sort(int arr[], int left, int right, int *tmp)
{
assert(arr);
assert(tmp);
int mid = left + (right - left)/2;
int i = left;
int j = mid;
int k = left;
while (i < mid && j < right)
{
if (arr[i] < arr[j])
{
tmp[k++] = arr[i++];
}
else
{
tmp[k++] = arr[j++];
}
}
while (i < mid)
{
tmp[k++] = arr[i++];
}
while (j < right)
{
tmp[k++] = arr[j++];
}
memcpy(arr + left, tmp + left, sizeof(int)* (right - left));
}
void sort(int arr[], int left, int right, int *tmp)
{
assert(arr);
assert(tmp);
if (right- left<=1)
{
return;
}
int mid = left +(right-left)/2;
sort(arr,left,mid,tmp);
sort(arr,mid, right,tmp);
_Merge_Sort(arr,left,right,tmp);
}
void Merge_Sort(int arr[], int size)
{
assert(arr);
int *tmp = (int*)malloc(sizeof(arr)*size);
sort(arr,0, size,tmp);
free(tmp);
}
时间复杂度:
O(NlogN)
空间复杂度:
O(1)
快速排序
快速排序:
对冒泡排序的改进,通过一个标准值(一般为最左或最右),将比它小的都放置左边,比它大的都放在右边,然后更改标准值再次分段进行排序
void Swap2(int* a, int* b) {
assert(a);
assert(b);
int tmp = *a;
*a = *b;
*b = tmp;
}
int Partion(int arr[], int _left, int _right) {
assert(arr);
int left = _left;
int right = _right - 1;
int key = arr[right];
while (left < right) {
/*
**从左往右找到比key值大的元素
*/
while (left < right && arr[left] <= key) {
++left;
}
/*
**从右往左找到比key值小的元素
*/
while (left < right && arr[right] >= key) {
--right;
}
/*
**交换
*/
if (left < right) {
Swap2(&arr[left], &arr[right]);
}
}
/*
**此时left指向的值一定大于key
**如果是以为++left导致循环退出(如果没找到,那么肯定left >= right)
**而right在上一次循环中已经是一个大于key的值了
**如果是因为--right导致循环退出(和上面的情况一样)
**left在上面的循环中已经找到了大于key的值
*/
//所以left指向的值一定大于key
Swap2(&arr[left], &arr[_right - 1]);
return left;
}
void QuickSort1(int arr[], int left, int right) {
assert(arr);
if (right - left <= 1) {
return;
}
/*
**具体思路如下:
**每次都找出一个基准key值,然后把数组分为两半
**左边是小于key的元素,右边是大于key的元素
**再进行递归的处理,直到所有的元素都有序了
*/
int mid = Partion(arr, left, right);
QuickSort1(arr, left, mid);
QuickSort1(arr, mid + 1, right);
}
时间复杂度:
在最差情况下,划分由 n 个元素构成的数组需要进行 n 次比较和 n 次移动。因此划分所需时间为 O(n) 。最差情况下,每次主元会将数组划分为一个大的子数组和一个空数组。这个大的子数组的规模是在上次划分的子数组的规模减 1 。该算法需要 (n-1)+(n-2)+…+2+1= O(n^2) 时间。
在最佳情况下,每次主元将数组划分为规模大致相等的两部分。设 T(n) 表示使用快速排序算法对包含 n 个元素的数组排序所需的时间,因此,和归并排序的分析相似,快速排序的 T(n)= O(nlogn)。
空间复杂度:
O(logn)
排序算法的稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的
对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法
堆排序、快速排序、希尔排序、直接选择排序不是稳定的排序算法
冒泡排序、直接插入排序、归并排序是稳定的排序算法