插入排序:
插入排序和打牌一样,核心:数小往前插,数大则不管。一个数只和它前面的那个数进行比较,如果比前面的数小,则该元素往前边插去(交换位置),然后继续向前进行新的比较;
如果比前面的数大,则不将该元素往前插入(即,不管该数了),直接进行去轮询到(后边的)下一个元素;
void InsertSort(int* a, int len)
{
assert(a);
for (int i = 1; i < len; i++)
{
int end = i - 1;
int tmp = a[i];
while(end>=0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
时间复杂度:O(N^2)
希尔排序:
希尔排序可以看成是插入排序的改良,由于插入排序最好情况下的时间复杂度是O(N),在一小段数据比较顺序的时候,他的时间复杂度是比较小的。因此,希尔排序对插入排序做出了预排序的操作,所谓预排序,就是间隔分组排序好,然后不断缩小间隔,同时不断使我们的数据趋向顺序,然后等到间隔为一时,使其进行插入排序的数据的顺序相对有序,从而减少了插排消耗的时间。
void ShellSort(int* a, int len)
{
assert(a);
int gap = len;
while (gap > 1)
{
gap /= 2;
for (int i = 0; i < gap; i++)
{
for (int j = i+gap; j < len; j += gap)
{
int end = j - gap;
int tmp = a[j];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
}
相对来说,希尔排序的时间复杂度是O(NlogN)
选择排序:
选择排序,顾名思义就是,不断的遍历当前未排好序的数据,从中选出这组数据中最小的数与最大的数,然后把最小的数与未排好序的数据的头交换,把最大的数与未排好序的数据的尾交换,对尾减一,对头加一。直到头尾相遇就结束循环。
注意:选择排序的思路比较直接,但是仍旧需要注意在把最小的数与未排好序的数据的头交换,把最大的数与未排好序的数据的尾交换的时候,如果当前指向最大的数的下标与未排好序的数据的头下标相等,一旦把最小的数与未排好序的数据的头交换,那么就会改变原本指向最大的数的下标的数据,因此需要进行特殊处理。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//选择排序
void SelectSort(int* a, int len)
{
assert(a);
int left = 0;
int right = len - 1;
while (left < right)
{
int mini = left;
int maxi = left;
for (int i = left; i <= right; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[mini], &a[left]);
if (left == maxi)
{
maxi = mini;
}
Swap(&a[maxi], &a[right]);
left++;
right--;
}
}
选择排序是性能最差的排序,时间复杂度是O(N^2)
堆排序:
堆排序,先建堆,后通过在堆上删除数据的思想,不断的把当前堆上最大值往后移!在建堆上有俩种方法,一种是向上调整建堆,一种是向下调整建堆!
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//堆排
void AdjustDown(int* a, int len,int parent)
{
assert(a);
int child = parent * 2 + 1;
while (child < len)
{
if (child + 1 < len && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void AdjustUp(int* a, int child)
{
assert(a);
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HeapSort(int* a, int len)
{
assert(a);
//向下调整建堆
/*for (int i = (len - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, len, i);
}*/
//向上调整建堆
for (int i = 0; i < len; i++)
{
AdjustUp(a, i);
}
int end = len - 1;
while (end)
{
Swap(&a[end], &a[0]);
AdjustDown(a, end, 0);
end--;
}
}
堆排序的时间复杂度:O(NlogN)
冒泡排序:
冒泡排序就是不断的把大的数往上浮把小的数向下沉的过程。
void BubbleSort(int* a, int len)
{
assert(a);
for (int i = 0; i < len; i++)
{
bool exchange = false;
for (int j = 1; j < len - i; j++)
{
if (a[j - 1] > a[j])
{
Swap(&a[j - 1], &a[j]);
exchange = true;
}
}
if (exchange == false)
{
break;
}
}
}
时间复杂度:O(N^2)
快速排序:
先从数据序列中选一个元素,并将序列中所有比该元素小的元素都放到它的右边或左边,再对左右两边分别用同样的方法处之直到每一个待处理的序列的长度为1,处理结束。
快速排序的第一种方法:Hoare法
先申明左右俩端下标,对左右俩端开始相向遍历,如果右端遇见比选中的元素小的时候,停下来,如果左端遇见比选中的元素大时,也停下来,然后对着俩下标指向的元素交换,然后继续相向而行。直到他们相遇,而这时候相遇的元素必然比指定元素小,因此就可以将指定元素与相遇时的元素相互交换。这就是一次遍历,然后不断递归。
int PartSort1(int* a, int left, int right)
{
assert(a);
//随机取值
//int tmp = rand() % (right - left + 1) + left;
//Swap(&a[left], &a[tmp]);
//三数取中
int tmp = MidNumber(a, left, right);
Swap(&a[left], &a[tmp]);
int begin = left;
int end = right;
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
return keyi;
}
快速排序的第二种方法:挖坑法
先声明一个变量等于指定的元素,存储该指定元素,同样申明左右俩端下标,对左右俩端开始相向遍历,如果右端遇见比选中的元素小的时候,把当前右端下标指向的元素填补当前坑位指向的位置,然后把右端下标视为坑位,如果左端遇见比选中的元素大时,把当前左端下标指向的元素填补当前坑位指向的位置,然后再把左端下标视为坑位。直到他们俩个相遇的时候,这时候坑位指向的位置就是开始存储那个指定元素在这一串数据中正确的位置。然后再不断的递归
int PartSort2(int* a, int left, int right)
{
assert(a);
//随机取值
//int tmp = rand() % (right - left + 1) + left;
//Swap(&a[left], &a[tmp]);
//三数取中
int tmp = MidNumber(a, left, right);
Swap(&a[left], &a[tmp]);
int key = a[left];
int begin = left;
int end = right;
int hole = left;
while (left < right)
{
while (left<right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
快速排序第三种方法:前后指针法
前后指针法就是,声明俩个下标,前指针一个初始化为左端下标,后指针初始化为左端下标的下一个。后指针不断遍历当前数据,当遇见必指定元素小时,停下来,然后前指针向前走一步,再将前后指针当前指向的元素交换。交换完后再后指针再继续遍历,直到后指针超过右端下标就停下来,这时候前指针指向的位置就是指定的元素排序好的正确位置,这时候交换。然后再不断递归。
int PartSort3(int* a, int left, int right)
{
assert(a);
//随机取值
//int tmp = rand() % (right - left + 1) + left;
//Swap(&a[left], &a[tmp]);
//三数取中
int tmp = MidNumber(a, left, right);
Swap(&a[left], &a[tmp]);
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
keyi = prev;
return keyi;
}
快速排序的弊端与优化:
快排的弊端是,当快排是有序的情况下,快排的时间复杂度反而很大,达到了O(N^2)的层次。这是因为指定的元素的原因。要想改善这一情况,有俩种方法随机取指定元素和三数取中法。
int MidNumber(int* a, int left, int right)
{
int mid = (right - left + 1) / 2;
if (a[mid] < a[left])
{
if (a[left] < a[right])
{
return left;
}
else if (a[mid] > a[right])
{
return mid;
}
else
{
return right;
}
}
else//mid>=left
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[right] < a[left])
{
return left;
}
else
{
return right;
}
}
}
当数据量很大的时候,快排递归的次数是非常大的,这时候其实可以通过在快排过程中,适当插入一些其他的排序,减少递归次数,优化快排。
void InproveQSort(int* a, int left, int right)
{
assert(a);
if (left >= right)
{
return;
}
if (right - left + 1 < 10)
{
//int keyi= PartSort1(a, left, right);
//int keyi = PartSort2(a, left, right);
int keyi = PartSort3(a, left, right);
InproveQSort(a, left, keyi - 1);
InproveQSort(a, keyi + 1, right);
}
else
{
InsertSort(a + left, right - left + 1);
}
}
当数据量非常大的时候,快排递归得深度非常打时候,这时候就需要使用快排的非递归模式。
快排的非递归首先就需要我们的栈来配合实现。
用栈实现快排的非递归的思路是:
先对当前要进行的快排的左右区间进行存储,当要进行排序的时候,取出当前存储的区间,当单次排序好后,再去对左右区间存入栈中,直到区间为一时就不存储了。然后不断取出栈中元素,直到栈空时间,排序也就好了。
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* top;
int size;
int capatipy;
}ST;
void StackInit(ST* ps);
void StackPush(ST* ps, STDataType x);
void StackPop(ST* ps);
bool StackEmpty(ST* ps);
void StackDestory(ST* ps);
STDataType StackTop(ST* ps);
void StackInit(ST* ps)
{
assert(ps);
STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * 4);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
ps->top = tmp;
ps->size = 0;
ps->capatipy = 4;
tmp = NULL;
}
void StackPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->size == ps->capatipy)
{
STDataType* tmp = (STDataType*)realloc(ps->top, sizeof(STDataType) * 2*(ps->capatipy));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->top = tmp;
ps->capatipy *= 2;
tmp = NULL;
}
ps->top[ps->size] = x;
ps->size++;
}
void StackPop(ST* ps)
{
assert(ps);
assert(!StackEmpty(ps));
ps->size--;
}
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->size == 0;
}
void StackDestory(ST* ps)
{
assert(ps);
free(ps->top);
ps->size = ps->capatipy = 0;
ps->top = NULL;
}
STDataType StackTop(ST* ps)
{
return ps->top[ps->size - 1];
}
void QuickSortNonR(int* a, int left, int right)
{
if (left >= right)
{
return;
}
ST s;
int keyi = 0;
StackInit(&s);
StackPush(&s, right);
StackPush(&s, left);
while (!StackEmpty(&s))
{
left = StackTop(&s);
StackPop(&s);
right = StackTop(&s);
StackPop(&s);
keyi = PartSort3(a, left, right);
if(keyi+1<right)
{
StackPush(&s, right);
StackPush(&s, keyi + 1);
}
if(left<keyi-1)
{
StackPush(&s, keyi - 1);
StackPush(&s, left);
}
}
StackDestory(&s);
}
归并排序:
建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序会先通过深度递归分为区间为1的子序列,再把子序列一区间为一一一归并,再以区间为二进行二二归并。就此以往,不管的归并回去。直到完成从左半区与右半区归并成为一个排序好的新序列。
void MergeSort(int* a, int* tmp, int left, int right)
{
assert(a);
assert(tmp);
if (left >= right)
{
return;
}
int mid = (right - left)/ 2+left;
MergeSort(a, tmp, left, mid);
MergeSort(a, tmp, mid + 1, right);
int i = left;
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}
归并排序的非递归:
归并排序的非递归与快速排序的非递归不同的是,快排的实现原理是一边排序一边递归,而归并排序的实现原理是先进行深度递归,再进行归并排序,因此进行深度递归,会使区间产生分裂,而栈很难同时跟上在递归完这一次后同时存储下俩个分裂区间。但是观察发现,归并排序的递归的区间是以1为首项,2为公比等比数列,因此,可以使用区间的值去控制。这样子,就可以直接实现归并的操作,一区间的值去不断阔大去控制归并区间,达到了归并排序的归并步骤。从某种意义上是从一开始就已经实现深度递归。
void MergeSortNonR(int* a, int* tmp, int left, int right)
{
assert(a);
assert(tmp);
int len = right - left + 1;
int gap = 1;
while (gap < len)
{
for(int i=left;i<=right;i+=2*gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int j = i;
if (end1 >= right)
{
end1 = right;
begin2 = end2 + 1;
}
if (end2 > right)
{
end2 = right;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i+1));
}
gap *= 2;
}
}
计数排序:
计数排序是一种非比较排序,他适合于小范围的数据。他是通过下标存储数据的方式再取出。
void CountSort(int* a, int len)
{
assert(a);
int mini = a[0];
int maxi = a[0];
for (int i = 0; i < len; i++)
{
if (a[i] < mini)
{
mini = a[i];
}
if (a[i] > maxi)
{
maxi = a[i];
}
}
int* tmp = (int*)malloc(sizeof(int) * (maxi - mini + 1));
memset(tmp, 0, sizeof(int) * (maxi-mini+1));
for (int i = 0; i < len; i++)
{
tmp[a[i] - mini]++;
}
int j = 0;
int i = 0;
while (j < len)
{
while(tmp[i]!=0)
{
a[j++] = i + mini;
tmp[i]--;
}
i++;
}
free(tmp);
tmp = NULL;
}
分析:这是一个计数排序的实现代码。计数排序是一种线性时间复杂度的排序算法,适用于待排序元素范围较小的情况。在这段代码中,CountSort
函数接受一个整型数组 a
和数组的长度 len
作为参数。首先,我们找出数组中的最小值 mini
和最大值 maxi
,以确定计数数组 tmp
的大小。然后,我们创建一个大小为 maxi - mini + 1
的计数数组 tmp
,并将其初始化为零。接下来,我们遍历数组 a
,将每个元素的值减去 mini
,并在计数数组 tmp
中对应的位置加一,表示该值的出现次数。然后,我们使用两个指针 i
和 j
,将计数数组 tmp
中的非零元素按顺序放回原数组 a
中,恢复原来的值。最后,我们释放计数数组 tmp
的内存,并将其指针置为空。这样,经过计数排序后,原数组 a
中的元素将按升序排列。