文章目录
前言
当作考试前复习一下了。都是非递归写法实现的,都只实现升序。此处需要排序的都是 int
型的数组,而且都是从0开始存的数字。所有的排序函数都只需要两个参数:数组的头指针和数组的长度。
直接插入排序(insert sort)
时间复杂度为O(n²),最坏时间复杂度也为O(n²)。空间复杂度为O(1)。算法是稳定的。
实现如下:
void insert_sort(int *a, int n)
{
int temp;
for (int i = 1; i < n; ++ i)
{
temp = a[i];
int j = i;
for (; j >= 1; -- j)
{
if (a[j - 1] > temp)
a[j] = a[j - 1];
else
break;
}
a[j] = temp;
}
}
折半插入排序(binary insert sort)
折半插入排序是队直接插入排序的改进。通过二分查找的方式来更快在已经排好序额子列中找打需要插入的位置。但是移动次数没有变化,所以时间复杂度与空间复杂度与直接插入一致,不过会更快一点。
实现如下:
void binary_insert_sort(int *a, int n)
{
int temp;
for (int i = 1; i < n; ++ i)
{
temp = a[i];
int left = 0, right = i - 1;
while (left <= right)
{
int mid = (left + right) / 2;
if (a[mid] > temp)
right = mid - 1;
else
left = mid + 1;
}
for (int j = i; j > right; -- j)
a[j] = a[j - 1];
a[right + 1] = temp;
}
}
希尔排序(shell sort)
希尔排序是对直接插入排序的一个较大的优化,基本思想是通过不断使用直接插入让间隔为d的子串先有序,那么这样就可以使得要排序的序列变得“大致有序”,而“大致有序”的序列使用直接插入排序会很快。
希尔排序的时间复杂度分析是所有排序算法中最难的,在使用了理想的增量序列的条件下,希尔排序的时间复杂度和最坏时间复杂度都为O(n² ³)。由于是在本地排序的,所以空间复杂度是O(1)。但是算法是不稳定的。
实现如下:
void shell_insert(int *a, int n, int d) // d为该趟shell排序的增量
{
int temp;
for (int i = d; i < n; ++ i) // 相当于将直接插入排序中的几个“1”换成“d”,其余没有区别
{
temp = a[i];
int j = i;
for (; j >= d; j -= d)
{
if (a[j - d] > temp)
a[j] = a[j - d];
else
break;
}
a[j] = temp;
}
}
void shell_sort(int *a, int n)
{
int Hibbard[] = {1, 3, 7, 15, 31, 63, 12, 511, 1023, 2047}; // 使用Hibbard增量序列,也就是2^k-1
int d[50], k = -1;
// 根据n截取增量序列到d中去
for (int i = 0; Hibbard[i] <= n / 2; ++ i)
d[++ k] = Hibbard[i];
// 开始shell排序,需要注意的是,d需要从大到小,所以我们需要倒着使用d数组
for (int i = k; i >= 0; -- i)
shell_insert(a, n, d[i]);
}
冒泡排序(bubble sort)
这个不用多说了,每趟冒泡会使得剩余无序序列中最大的那个元素达到最末尾。时间复杂度和最坏时间复杂度都为O(n²)。是稳定的排序算法。
实现如下:
void bubble_sort(int *a, int n)
{
for (int i = 0; i < n; ++ i)
{
for (int j = 1; j < n - i; ++ j)
if (a[j - 1] > a[j])
swap(a[j - 1], a[j]);
}
}
快速排序(quick sort)
快排的思想是寻找“信标”,也就是一趟快排操作能够保证所操作的这段子列的开头的元素一定落在排好序的地方,这个很容易办到:因为有序升序序列的基本性质是任意一个元素的左边所有元素一定比它小,右边的所有元素一定比它大,因此我们只需要在快排时保证我们考察的“信标”的左边的元素小于它,右边的元素大于它就行。这样找到后,在对分割后的序列的两边递归得使用这一操作就行了。
很明显,若原序列是有序得,那么快排需要将每个信标都遍历整个数组,此时的时间复杂度为O(n²)。而对于一般得情况,由于是对每个拆分得两段子列递归使用快排,因此是O(n log n)。且快排是不稳定的。
实现如下:
int Partition(int *a, int low, int high) // 将信标摆放到正确的位置
{
int pivot = a[low];
while (low < high)
{
while (low < high && a[high] >= pivot)
high --;
a[low] = a[high];
while (low < high && a[low] <= pivot)
low ++;
a[high] = a[low];
}
a[low] = pivot;
return low;
}
void quick_sort(int *a, int n)
{
int *low = new int[n], p_low; // 使用栈的非递归写法
int *high = new int[n], p_high;
int top = 0, loc;
low[top] = 0;
high[top] = n - 1;
while (top >= 0)
{
p_low = low[top];
p_high = high[top];
top --;
loc = Partition(a, p_low, p_high);
if (loc - 1 > p_low)
{
top ++;
low[top] = p_low;
high[top] = loc - 1;
}
if (loc + 1 < p_high)
{
top ++;
low[top] = loc + 1;
high[top] = p_high;
}
}
delete[] low;
delete[] high;
}
简单选择排序(selection sort)
选择排序的思想也很简单:从头至尾,每次选择剩余无需子列中最小的元素放到开头。时间复杂度与最坏时间复杂度为O(n²)。空间复杂度为O(1)。算法不稳定。
实现如下:
void selection_sort(int *a, int n)
{
int min_v, min_idx;
for (int i = 0; i < n; ++ i)
{
min_v = a[i];
min_idx = i;
for (int j = i + 1; j < n; ++ j)
{
if (a[j] < min_v)
{
min_v = a[j];
min_idx = j;
}
}
swap(a[i], a[min_idx]);
}
}
堆排序(heap sort)
使用堆这种数据结构来协助完成排序。时间复杂度和最坏时间复杂度为O(n log n)。空间复杂度为O(1),但是算法不稳定。
实现如下:
void adjust(int *a, int low, int high) // 在原本有序的大根堆中,改变堆顶元素后,调用该函数可以使得大根堆再次有序
{
int temp = a[low];
int i = low, j = 2 * i + 1;
while (j <= high)
{
if (j + 1 <= high && a[j + 1] > a[j])
j ++;
if (temp >= a[j])
break;
a[i] = a[j];
i = j, j = 2 * i + 1;
}
a[i] = temp;
}
void heap_sort(int *a, int n)
{
for (int i = (n - 2) / 2; i >= 0; -- i)
adjust(a, i, n - 1);
for (int i = n - 1; i >= 1; -- i)
{
swap(a[0], a[i]);
adjust(a, 0, i - 1);
}
}
归并排序(merge sort)
归并排序的思路是几个小的序列先归并成一个中等的有序序列,这几个中等的有序序列最后再归并为整个序列。时间复杂度和最坏时间复杂度都为O(n log n),空间复杂度为O(n),这是归并临时数组的额外开支。归并排序是稳定的算法。
由于递归写法会占用大量辅助空间,所以一般会使用非递归写法:
void Merge(int *a, int low, int mid, int high)
{
// 数据分割成两路:[low, mid]和[mid + 1, high]
int *t = new int[high - low + 1];
int i = low, j = mid + 1, k = 0;
while (i <= mid && j <= high)
{
if (a[i] < a[j])
t[k ++] = a[i ++];
else
t[k ++] = a[j ++];
}
// 可能会有剩余的
while (i <= mid) t[k ++] = a[i ++];
while (j <= high) t[k ++] = a[j ++];
// 物归原主
for (i = low, k = 0; i <= high; ++ i, ++ k)
a[i] = t[k];
delete[] t;
}
void MSort(int *a, int n, int len) // 进行长度为len的一趟归并排序
{
int i = 0;
while (i + 2 * len < n)
{
Merge(a, i, i + len - 1, i + 2 * len - 1);
i += 2 * len;
}
// 可能会有残余的无法组成一个len的序列
if (i + len < n - 1) // 由于mid铁定小于high
Merge(a, i, i + len - 1, n - 1);
}
void merging_sort(int *a, int n)
{
for (int len = 1; len < n; len *= 2)
MSort(a, n, len);
}
基数排序(radix sort)
简单说就是入队与收集,一般会考虑基数为10的情况,也就是按照十进制数来处理,当然,将基数设为2是最高效的写法。基数排序只适用于范围和数量有限的序列。
时间复杂度和最坏时间复杂度为O(dn),其中的d是选择的基数。空间复杂度为O(rd),基数排序是稳定的算法。
实现代码如下:
struct Queue
{
int a[1000];
int front, rear;
Queue() {front = rear = 0;}
~Queue() {}
int size() {return rear - front;}
bool empty() {return !(bool)size();}
int pop() {return a[front ++];}
void push(int v) {a[rear ++] = v;}
};
void radix_sort(int *a, int n)
{
Queue Q[10];
int radix = 1;
for (int k = 0; k < 3; ++ k)
{
radix *= 10;
for (int i = 0; i < n; ++ i)
{
int m = (a[i] % radix) / (radix / 10);
Q[m].push(a[i]);
}
for (int m = 0, i = 0; m < 10; ++ m)
{
while (!Q[m].empty())
a[i ++] = Q[m].pop();
}
}
}