插入排序
直接插入排序
原理:将数组分为无序区和有序区两个区,然后不断将无序区的第一个元素按大小顺序插入到有序区中去,最终将所有无序区元素都移动到有序区完成排序。
void insertSort(int a[], int N)
{
for(int i = 1; i < N; i++)
{
int temp = a[i];
int j;
for(j = i; j > 0 && temp < a[j-1]; j--)
a[j] = a[j-1];
a[j] = temp;
}
}
希尔排序
原理:又称增量缩小排序。先将序列按增量划分为元素个数相同的若干组,使用直接插入排序法进行排序,然后不断缩小增量直至为1,最后使用直接插入排序完成排序。
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
void shellSort(int a[], int N)
{
for(int incre = N / 2; incre > 0; incre /= 2)
{
for(int i = incre; i < N; i++)
{
int temp = a[i];
int j;
for(j = i; j >= incre && temp < a[j - incre]; j -= incre)
a[j] = a[j - incre];
a[j] = temp;
}
}
}
交换排序
冒泡排序
原理:将序列划分为无序和有序区,不断通过交换较大元素至无序区尾完成排序。
void bubbleSort(int a[], int N)
{
for(int i = 0; i < N; i++)
{
for(int j = i; j < N - i - 1; j++)
{
if(a[j] > a[j+1])
{
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
补充说明:使用didSwap=true/false可以避免重复的比较,使得最好情况的复杂度变为O(n)
快速排序
原理:不断寻找一个序列的中点,然后对中点左右的序列递归的进行排序,直至全部序列排序完成,使用了分治的思想。
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
实现:
void quickSortCore(int a[], int left, int right)
{
if(left < right)
{
int temp = a[left];
int i = left, j = right;
while(i < j)
{
while(i < j && temp <= a[j])
j--;
if(i < j)
a[i++] = a[j];
while(i < j && temp >= a[i])
i++;
if(i < j)
a[j--] = a[i];
}
a[i] = temp;
quickSortCore(a, left, i-1);
quickSortCore(a, i+1, right);
}
}
void quickSort(int a[], int N)
{
quickSortCore(a, 0, N-1);
}
选择排序
直接选择排序
原理:将序列划分为无序和有序区,寻找无序区中的最小值和无序区的首元素交换,有序区扩大一个,循环最终完成全部排序。
void selectSort(int a[], int N)
{
for(int i = 0; i < N; i++)
{
int k = i;
for(int j = i + 1; j < N; j++)
{
if(a[j] < a[k])
k = j;
}
int temp = a[i];
a[i] = a[k];
a[k] = temp;
}
}
堆排序
堆序性质: 堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]
称为大顶堆,满足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]
称为小顶堆。
原理:利用大顶堆或小顶堆思想,首先建立堆,然后将堆首与堆尾交换,堆尾之后为有序区。如从小到大排序,建立大顶堆,堆顶元素与堆尾不断交换,同时缩小堆的范围,最终得到排序结果。
void percDown(int a[], int i, int N)
{
int temp = a[i], child;
for(; 2 * i + 1 < N; i = child)
{
child = 2 * i + 1;
if(child + 1 < N && a[child] < a[child + 1])
child++;
if(temp < a[child])
a[i] = a[child];
else
break;
}
a[i] = temp;
}
void heapSort(int a[], int N)
{
for(int i = N / 2; i >= 0; i--)
percDown(a, i, N);
for(int i = N-1; i > 0; i--)
{
int temp = a[i];
a[i] = a[0];
a[0] = temp;
percDown(a, 0, i);
}
}
归并排序
原理:将原序列划分为有序的两个序列,然后利用归并算法进行合并,合并之后即为有序序列。
void merge(int a[], int temp[], int left, int mid, int right)
{
if(left < right)
{
int lpos = left, lend = mid;
int rpos = mid + 1, rend = right;
int tpos = left;
while(lpos <= lend && rpos <= rend)
{
if(a[lpos] <= a[rpos])
temp[tpos++] = a[lpos++];
else
temp[tpos++] = a[rpos++];
}
while(lpos <= lend)
temp[tpos++] = a[lpos++];
while(rpos <= rend)
temp[tpos++] = a[rpos++];
for(int i = 0; i <= right; i++)
a[i] = temp[i];
}
}
void mergeSortCore(int a[], int temp[], int left, int right)
{
if(left < right)
{
int mid = (left + right) / 2;
mergeSortCore(a, temp, left, mid);
mergeSortCore(a, temp, mid+1, right);
merge(a, temp, left, mid, right);
}
}
void mergeSort(int a[], int N)
{
int *temp = new int[N];
mergeSortCore(a, temp, 0, N-1);
delete [] temp;
}
各种排序算法的复杂度稳定性分析
分类 | 名称 | 复杂度分析 | 稳定性 | 稳定性原因分析 |
---|---|---|---|---|
插入排序 | 简单插入 | 平均O(n^2),最好O(n),最坏O(n^2) | 稳定 | 没有跨元素交换 |
———- | 希尔排序 | 平均接近nlogn,最好O(n),最坏O(n^2) | 不稳定 | 增量分组,有跨元素交换 |
交换排序 | 冒泡排序 | 平均O(n^2),最好O(n),最坏O(n^2) | 稳定 | 没有跨元素交换 |
———- | 快速排序 | 平均nlogn,最好nlogn,最坏O(n^2) | 不稳定 | 有跨元素交换 |
选择排序 | 直接选择 | 平均O(n^2),最好O(n^2),最坏O(n^2) | 不稳定 | 5 8 5 2 |
———- | 堆排序 | 平均,最好,最坏nlogn | 不稳定 | 3 2 3 2 |
归并排序 | 归并排序 | 平均,最好,最坏nlogn,有O(n)空间复杂度 | 稳定 | 没有跨元素交换 |
扩展问题
- 单链表可以做快速排序吗?为什么?
可以。快速排序的核心函数partition,选择某个元素为枢纽元x(通常是第一个),一遍扫描之后使得比x小的在枢纽元左边,比x大的在枢纽元的右边。使用链表时,x指向链表头(枢纽元),扫描这个链表,小元素拼在链表头,大元素拼在链表尾部,从而完成一次partition函数的流程。