一、冒泡排序
个人理解:
冒泡排序是最基础的排序算法之一,其思想就是把一个最大值排到最后面,通过不断排出最大值,来达到“冒泡”的效果。
代码思想:
重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
复杂度和稳定性分析:
时间复杂度O(n^2)
空间复杂度O(1)
不稳定
代码:
//冒泡排序
void BubbleSort(int *arr, int len)
{
for (int i = 0; i < len - 1; i++)
{
for (int j = 0; j < len - i - 1; ++j)
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
}
}
}
}
二、选择排序
个人理解:
选择排序也是最基础的排序算法之一,其基本思想是选择一个最小值(或者最大值),然后把它直接放到该放的地方。
代码思想:
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
复杂度和稳定性分析:
时间复杂度O(n^2)
空间复杂度O(1)
不稳定
代码:
//选择排序
void SelectSort(int *arr, int len)
{
for (int i = 0; i < len - 1; ++i)
{
int min = i;
for (int j = i; j < len; ++j)
{
if (arr[min] > arr[j])
{
min = j;
}
}
Swap(&arr[i], &arr[min]);
}
}
三、直接插入排序
个人理解:
直接插入排序本质上是把要排序的数组拆分成一个一个独立的元素,通过不断“插入”后面的元素来实现排序。
代码思想:
每一趟将一个待排序的记录,按其关键字的大小插入到已经排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。
复杂度和稳定性分析:
时间复杂度O(n^2)
空间复杂度O(1)
稳定
代码:
//直接插入排序
void InsertSort(int *arr, int len)
{
for (int i = 1; i < len; ++i)
{
int tmp = arr[i];
int j = i - 1;
while (arr[j] > tmp && j >= 0)
{
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = tmp;
}
}
四、希尔排序
个人理解:
希尔排序又称为“缩小增量排序”,是直接插入排序的优化算法,通过分组的形式先隔开进行直接排序,通过并组的方式进行整体的排序,这种方法的优点在于,假设A<B,C<D,那么A<D的概率就会增加,数组会趋于有序,会减少交换的次数。
代码思想:
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量 =1( < …<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
复杂度和稳定性分析:
时间复杂度 O(n^1.3) ~~ O(n^1.5)
空间复杂度O(1)
不稳定
图解希尔排序:
代码:
//希尔排序
void Shell(int *arr, int len, int d)
{
for (int i = d; i < len; ++i)
{
int tmp = arr[i];
int j = i - d;
while (arr[j] > tmp && j >= 0)
{
arr[j + d] = arr[j];
j -= d;
}
arr[j + d] = tmp;
}
}
void ShellSort(int *arr, int len)
{
int d[4] = { 7, 5, 3, 1 };//必须互质
for (int i = 0; i < 4; ++i)
{
Shell(arr, len, d[i]);
}
}
五、堆排序
个人理解:
堆排序我个人认为,其实跟利用堆结构的选择排序差别不大,利用堆结构使数组本身大致有序,达到优化的作用。
代码思想:
创建一个堆之后,把根节点与最后一个元素互换,在不计入最后一个元素的情况下,根据新的结构改变堆,依次类推,最后达到排序的效果。
复杂度和稳定性分析:
时间复杂度O(nlogn)
空间复杂度O(1)
不稳定
图解堆排序:
代码:
//堆排序
void Adjust(int *arr, int len, int root)
{
int j = 2 * root + 1;
while (j < len)
{
if (j + 1 < len && arr[j] < arr[j + 1])
{
j += 1;
}
if (arr[j] > arr[root])
{
Swap(&arr[j], &arr[root]);
}
root = j;
j = 2 * root + 1;
}
}
void CreateHeap(int *arr, int len)
{
for (int root = (len - 2) / 2; root >= 0; --root)
{
Adjust(arr, len, root);
}
}
void HeapSort(int *arr, int len)
{
CreateHeap(arr, len);
for (int i = len; i > 0; --i)
{
Swap(&arr[0], &arr[i - 1]);
Adjust(arr, i - 1, 0);
}
}
六、快速排序
个人理解:
快速排序其实就是一个分组方法的变种,以一个基准为界,把数组中的数据元素分为两组,然后再在两组中套用此方法,达到最终数组有序的目的。
代码思想:
递归- - -选择一个基数,把比基数小的都放在左边,比基数大的放在右边,以基数为界,把数组分为两个部分,递归重复上述过程,直到两边只剩一个数据元素不用排序为止。
复杂度和稳定性分析:
时间复杂度O(nlogn)
空间复杂度O(logn)
不稳定
图解快速排序:
代码:
//快速排序
int OneQuick(int *arr, int head, int tail)
{
int i = head,
j = tail;
int tmp = arr[i];
while (i < j)
{
while (tmp <= arr[j] && i < j)
{
j--;
}
if (i >= j)
{
break;
}
arr[i] = arr[j];
i++;
while (tmp >= arr[i] && i < j)
{
i++;
}
if (i >= j)
{
break;
}
arr[j] = arr[i];
j--;
}
arr[i] = tmp;
return i;
}
void Quick(int *arr, int head, int tail)
{
int i = OneQuick(arr, head, tail);
if (i - head > 1)
{
Quick(arr, head, i - 1);
}
if (tail - i > 1)
{
Quick(arr, i + 1, tail);
}
}
void QuickSort(int *arr, int len)
{
Quick(arr, 0, len - 1);
}
七、快速排序的优化
非递归
非递归的快速排序实际上是采用了栈的结构来用循环代替递归,把本来需要递归来处理划分出来的两个组的边界值存储在栈中,通过入栈出栈的方式来依次处理,达到消除递归的效果,这里代码引用了自己写的栈。
//快速排序
int OneQuick(int *arr, int head, int tail)
{
int i = head,
j = tail;
int tmp = arr[i];
while (i < j)
{
while (tmp <= arr[j] && i < j)
{
j--;
}
if (i >= j)
{
break;
}
arr[i] = arr[j];
i++;
while (tmp >= arr[i] && i < j)
{
i++;
}
if (i >= j)
{
break;
}
arr[j] = arr[i];
j--;
}
arr[i] = tmp;
return i;
}
void NiceQuick(int *arr, int head, int tail)
{
SqStack st;
InitStack(&st);
PushStack(&st, head);
PushStack(&st, tail);
while (!EmptyStack(&st))
{
int right = 0;
PopStack(&st, &right);
int left = 0;
PopStack(&st, &left);
int mod = OneQuick(arr, left, right);
if (mod - left > 1)
{
PushStack(&st, left);
PushStack(&st, mod - 1);
}
if (right - mod > 1)
{
PushStack(&st, mod + 1);
PushStack(&st, right);
}
}
}
void QuickSort(int *arr, int len)
{
NiceQuick(arr, 0, len - 1);
}
三数取中法
之前的快排一般都是取第一个值作为基准,但是如果这个值本身就是最大值或者最小值,那么此次的排序就毫无意义,极大的浪费了时间。
三数取中法即取第一个值、中间一个位置的值和最后一个值,把其中间值与第一个值交换作为基准来进行快排。
优化部分代码:
//快速排序
void SelectPivoMedianOfThree(int *arr, int head, int tail)
{
int mod = (tail - head) / 2 + head;
if (arr[mod] > arr[tail])
{
Swap(&arr[tail], &arr[mod]);
}
if (arr[head] > arr[tail])
{
Swap(&arr[head], &arr[tail]);
}
if (arr[head] < arr[mod])
{
Swap(&arr[head], &arr[mod]);
}
}
int OneQuick(int *arr, int head, int tail)
{
int i = head,
j = tail;
SelectPivoMedianOfThree(arr, head, tail);
int tmp = arr[i];
while (i < j)
{
while (tmp <= arr[j] && i < j)
{
j--;
}
if (i >= j)
{
break;
}
arr[i] = arr[j];
i++;
while (tmp >= arr[i] && i < j)
{
i++;
}
if (i >= j)
{
break;
}
arr[j] = arr[i];
j--;
}
arr[i] = tmp;
return i;
}
当快排小到一定程度使,使用直接插入排序
这个是因为,当数据量小到一定程度时,快排的效率不如直接插入排序好。
优化部分代码:
void Quick(int *arr, int head, int tail)
{
if (tail - head < 10)
{
InsertSort(arr + head, tail - head + 1);
return;
}
int i = OneQuick(arr, head, tail);
if (i - head > 1)
{
Quick(arr, head, i - 1);
}
if (tail - i > 1)
{
Quick(arr, i + 1, tail);
}
}
八、二路归并排序
个人理解:
二路归并排序实质上是把无序的数组用分治法分成数个小数组,把小数组变成有序的来逐步把整个数组变为有序。
代码思想:
把数组分成两两的数据段,两个数据段合并为有序的数据段,两个合并完的有序数据段再合并成一个有序的数据段,直至剩一个有序的数据段,即为所要排列的数组。
复杂度和稳定性分析:
时间复杂度O(nlogn)
空间复杂度O(n)
稳定
图解二路归并排序:
代码:
//二路归并排序
void Merge(int *arr, int len, int width)
{
int *brr = (int *)malloc(sizeof(int)* len);
assert(brr != NULL);
int low1 = 0;
int high1 = low1 + width - 1;
int low2 = high1 + 1;
int high2 = low2 + width - 1 < len - 1 ? low2 + width - 1 : len - 1;
int count = 0;
while (low2 < len)
{
//两个归并段
while (low1 <= high1 && low2 <= high2)
{
if (arr[low1] < arr[low2])
{
brr[count++] = arr[low1++];
}
else
{
brr[count++] = arr[low2++];
}
}
//只剩一个归并段
if (low1 <= high1)
{
while (high1 - low1 + 1)
{
brr[count++] = arr[low1++];
}
}
else
{
while (high2 - low2 + 1)
{
brr[count++] = arr[low2++];
}
}
low1 = high2 + 1;
high1 = low1 + width - 1 < len - 1 ? low1 + width - 1 : len - 1;
low2 = high1 + 1;
high2 = low2 + width - 1 < len - 1 ? low2 + width - 1 : len - 1;
}
while (high1 - low1 + 1)
{
brr[count++] = arr[low1++];
}
for (int i = 0; i < len; ++i)
{
arr[i] = brr[i];
}
free(brr);
}
void TwoMergeSort(int *arr, int len)
{
for (int i = 1; i < len; i *= 2)
{
Merge(arr, len, i);
}
}
九、基数排序
个人理解:
基数排序是多关键字的排序,其核心在于不同关键字的权重不一样,不同权重所带来的的优先级是不一样的,通过同一优先级先进行比较来区分数据。
代码思想:
申请关键字范围个数的队列,再循环根据各关键字将数据依次存储到对应的队列中,最后将队列依次输出。
复杂度与稳定性分析:
时间复杂度O(d(r+n))
空间复杂度O(rd+n)
稳定
图解基数排序:
代码:
//基数排序
int GetMaxDigits(int *arr, int len)
{
int maxValue = arr[0];
for (int i = 0; i < len; ++i)
{
if (arr[i] > maxValue)
{
maxValue = arr[i];
}
}
int digits = 0;
while (maxValue)
{
digits++;
maxValue /= 10;
}
return digits;
}
int GetDigitsValue(int value, int digits)
{
while (digits)
{
value /= 10;
digits--;
}
return value % 10;
}
void RadixSort(int *arr, int len)
{
//获取最大关键字的位数
int maxDigits = GetMaxDigits(arr, len);
//申请关键字范围个数的队列 0--9
SqQueue que[10];
for (int i = 0; i < 10; ++i)
{
InitQueue(&que[i]);
}
//循环根据各关键字将数据依次存储到对应的队列中,最后将队列依次输出
for (int i = 0; i < maxDigits; ++i)
{
//将所有的数据依次存储在对应队列中
for (int j = 0; j < len; ++j)
{
int digValue = GetDigitsValue(arr[j], i);
PushQueue(&que[digValue], arr[j]);
}
//将队列中的所有数据依次输出到arr中
int index = 0;
for (int k = 0; k < 10; ++k)
{
while (!EmptyQueue(&que[k]))
{
PopQueue(&que[k], &arr[index]);
index++;
}
}
}
for (int i = 0; i < 10; ++i)
{
ClearQueue(&que[i]);
}
}