这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
插入排序
- 基本思想:每步将一个待排序的纪录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。
- 算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。
- 代码:
void insertionSort(int arr[], int length){
int i,j;
for (i = 1; i < length; i++) {
int tmp = arr[i];
for (j = i; j > 0 && arr[j - 1] > tmp; j--) {
arr[j] = arr[j - 1];
}
arr[j] = tmp;
}
}
冒泡排序
- 基本思想:持续比较相邻的元素。如果第一个比第二个大,就交换他们两个。直到没有任何一对数字需要比较。
- 冒泡排序最好的时间复杂度为O(n)。冒泡排序的最坏时间复杂度为O(n^2)。因此冒泡排序总的平均时间复杂度为O(n^2)。
- 算法适用于少量数据的排序,是稳定的排序方法。
- 代码:
void bubbleSort(vector<int>& a){
bool swapp = true;
while(swapp){
swapp = false;
for (size_t i = 0; i < a.size()-1; i++) {
if (a[i]>a[i+1] ){
a[i] += a[i+1];
a[i+1] = a[i] - a[i+1];
a[i] -=a[i+1];
swapp = true;
}
}
}
}
选择排序
- 基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
- 选择排序是不稳定的排序方法。时间复杂度 O(n^2)。
- 代码:
void selectSort(int array[]){
int length = array.size();
for(int i = 0;i<length-1;i++){
int min = array[i];
int minindex = i;
for(int j = i;j<length;j++){
if(array[j]<min){ //选择当前最小的数
min = array[j];
minindex = j;
}
}
if(i != minindex){ //若i不是当前元素最小的,则和找到的那个元素交换
array[minindex] = array[i];
array[i] = min;
}
}
}
希尔排序
- 基本思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-1…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
- 在使用增量dk的一趟排序之后,对于每一个i,我们都有a[i]<=a[i+dk],即所有相隔dk的元素都被排序。
- 如图:增量序列为5,3,1,每一趟排序之后,相隔对应增量的元素都被排序了。当增量为1时,数组元素全部被排序。
- 希尔排序不稳定,时间复杂度 平均时间 O(nlogn) 最差时间O(n^2)
- 代码:
void shellSort(int arr[], int n) {
for (int gap = n/2; gap > 0; gap /= 2) {
for (int i = gap; i < n; i += 1) {
int temp = arr[i];
int j;
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap)
arr[j] = arr[j - gap];
arr[j] = temp;
}
}
}
堆排序
二叉堆是完全二元树(二叉树)或者是近似完全二元树(二叉树)。 二叉堆有两种:最大堆和最小堆。 大根堆:父结点的键值总是大于或等于任何一个子节点的键值; 小根堆:父结点的键值总是小于或等于任何一个子节点的键值。 二叉堆一般用数组来表示。例如,根节点在数组中的位置是0,第n个位置的子节点分别在2n+1和 2n+2。因此,第0个位置的子节点在1和2,1的子节点在3和4。以此类推。这种存储方式便於寻找父节点和子节点。如图,为最小堆。
建立堆函数(最大堆):
void heapify(int arr[], int n, int i) {
int largest = i; // 将最大元素设置为堆顶元素
int l = 2*i + 1; // left = 2*i + 1
int r = 2*i + 2; // right = 2*i + 2
// 如果 left 比 root 大的话
if (l < n && arr[l] > arr[largest])
largest = l;
// I如果 right 比 root 大的话
if (r < n && arr[r] > arr[largest])
largest = r;
if (largest != i) {
swap(arr[i], arr[largest]);
// 递归地定义子堆
heapify(arr, n, largest);
}
}
堆排序的方法如下,把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。
堆排序函数:
void heapSort(int arr[], int n) {
// 建立堆
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// 一个个从堆顶取出元素
for (int i=n-1; i>=0; i--) {
swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}
归并排序
- 归并排序的原理
- 将待排序的数组分成前后两个部分,再递归的将前半部分数据和后半部分的数据各自归并排序,得到的两部分数据,然后使用merge合并算法(算法见代码)将两部分算法合并到一起。 例如:如果N=1;那么只有一个数据要排序,N=2,只需要调用merge函数将前后合并,N=4,........... 也就是将一个很多数据的数组分成前后两部分,然后不断递归归并排序,再合并,最后返回有序的数组。
- 归并排序的时间复杂度
- 归并排序的最好、最坏和平均时间复杂度都是O(nlogn),而空间复杂度是O(n),比较次数介于(nlogn)/2和(nlogn)-n+1,赋值操作的次数是(2nlogn)。因此可以看出,归并排序算法比较占用内存,但却是效率高且稳定的排序算法。
- 代码:
void merge(int arr[], int l, int m, int r) {
int i, j, k;
int n1 = m - l + 1;
int n2 = r - m;
int L[n1], R[n2];
for (i = 0; i < n1; i++)
L[i] = arr[l + i];
for (j = 0; j < n2; j++)
R[j] = arr[m + 1+ j];
i = 0;
j = 0;
k = l;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
}
else{
arr[k] = R[j];
j++;
}
k++;
}
while (i < n1){
arr[k] = L[i];
i++;
k++;
}
while (j < n2){
arr[k] = R[j];
j++;
k++;
}
}
void mergeSort(int arr[], int l, int r){
if (l < r) {
int m = l+(r-l)/2;
mergeSort(arr, l, m);
mergeSort(arr, m+1, r);
merge(arr, l, m, r);
}
}
快速排序
-
快速排序原理:
-
如果数组S中元素是0或者1,则返回;
-
区数组S中任一元素v,称之为枢纽元;
-
将S-{v}(S中剩余的元素)划分成连个不相交的集合:S1={S-{v}|x<=v}和S2={S-{v}|x>=v};
-
返回{quicksort(s1)}后跟v,继而返回{quicksort(S2)}。
-
选取枢纽元(三数中值分割法)
-
一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基元。 分割策略: 在分割阶段吧所有小元素移到数组的左边,大元素移到数组右边。,大小是相对于枢纽元素而言的。 当i在j的左边时,将i右移,移过哪些小于枢纽元的元素,并将j左移,已过那些大于枢纽元的元素,当i和j停止时,i指向一个大元素,而j指向一个小元素,如果i在j的左边,那么将这两个元素交换,其效果是把一个大元素推向右边,而把小元素推向左边。
-
快速排序平均时间复杂度为O(nlogn),最坏情况为O(n^2),n越大,速度越快。不是稳定的排序算法。
-
代码:
//递归快速排序
void quickSort(int a[]){
qSort(a, 0, a.size() - 1);
}
//递归排序,利用两路划分
void qSort(int a[],int low,int high){
int pivot = 0;
if(low < high){
//将数组一分为二
pivot = partition(a,low,high);
//对第一部分进行递归排序
qSort(a,low,pivot);
//对第二部分进行递归排序
qSort(a,pivot + 1,high);
}
}
//partition函数,实现三数中值分割法
int partition(int a[],int low,int high){
int pivotkey = a[low]; //选取第一个元素为枢轴记录
while(low < high){
//将比枢轴记录小的交换到低端
while(low < high && a[high] >= pivotkey){
high--;
}
//采用替换而不是交换的方式操作
a[low] = a[high];
//将比枢轴记录大的交换到高端
while(low < high && a[low] <= pivotkey){
low++;
}
a[high] = a[low];
}
//枢纽所在位置赋值
a[low] = pivotkey;
//返回枢纽所在的位置
return low;
}
桶排序
-
桶排序的原理是将数组分到有限数量的桶中,再对每个桶子再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后将各个桶中的数据有序的合并起来。
-
排序过程:
- 假设待排序的一组数统一的分布在一个范围中,并将这一范围划分成几个子范围,也就是桶
- 将待排序的一组数,分档规入这些子桶,并将桶中的数据进行排序
- 将各个桶中的数据有序的合并起来
-
代码实现:
void bucketSort(float arr[], int n) {
vector<float> b[n];
for (int i=0; i<n; i++) {
int bi = n*arr[i];
b[bi].push_back(arr[i]);
}
for (int i=0; i<n; i++)
sort(b[i].begin(), b[i].end());
int index = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < b[i].size(); j++)
arr[index++] = b[i][j];
}
总结
| 排序法 | 平均时间 | 最差情形 | 稳定度 | 额外空间 | 备注 |
|---|---|---|---|---|---|
| 插入 | O(n2) | O(n2) | 稳定 | O(1) | 大部分已排序时较好 |
| 冒泡 | O(n2) | O(n2) | 稳定 | O(1) | n小时较好 |
| 选择 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
| Shell(希尔) | O(nlogn) | O(ns) | 不稳定 | O(1) | s是所选分组 |
| 堆 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时较好 |
| 归并 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 |
| 快速 | O(nlogn) | O(n2) | 不稳定 | O(nlogn) | n大时较好 |
| 桶排序 | O(k+n) | O(k+n) | 稳定 | O(1) | 只能排整形数组 |