冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个相邻元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
void BubbleSort(int* a, int n)
{
for (int j = 0;j < n;++j)
{
int flag = 1;
for (int i = 1;i < n - j;i++)
{
if (a[i - 1] > a[i])
{
flag = 0;
int tmp = a[i];
a[i] = a[i - 1];
a[i - 1] = tmp;
}
}
if (flag == 1)
{
break;
}
}
}
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
void insertsort(int* a, int n)
{
for (int i = 1;i < n; ++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;
}
}
1959年Shell发明; 第一个突破O(n²)的排序算法;是简单插入排序的改进版;它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
void Shellsort(int* a, int n)
{
int gap = n;
//1.gap > 1预排序
//2.gap == 1 直接插入排序
while(gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0;i < n - gap;i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
void swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int maxi = begin, mini = begin;
for (int i = begin;i <= end;i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
swap(&a[begin], &a[mini]);
if (begin == maxi)
{
maxi = mini;
}
swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
//向下调整
void AdjustDown(int* a, int n, int parent)
{
int child = parent / 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
//堆排序
void HeapSort(int* a, int n)
{
//升序 建大堆
//降序 建小堆
//向下调整建堆
for (int i = (n - 1 - 1) / 2;i >= 0;--i)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高! 它是处理大数据最快的排序算法之一了。快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
1、选出一个key,一般是最左边或是最右边的。
2、定义一个begin和一个end,begin从左向右走,end从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要end先走;若选择最右边的数据作为key,则需要bengin先走)。
3、在走的过程中,若end遇到小于key的数,则停下,begin开始走,直到begin遇到一个大于key的数时,将begin和right的内容交换,end再次开始走,如此进行下去,直到begin和end最终相遇,此时将相遇点的内容与key交换即可。(选取最左边的值作为key)
4.此时key的左边都是小于key的数,key的右边都是大于key的数
5.将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,此时此部分已有序
为避免每次key值都是最值。采取三数取中取到中间值作为key值
//三数取中
int GatMid(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
}
int PartSort(int* a, int left, int right)
{
int mid = GatMid(a, left, right);
swap(&a[left], &a[mid]);
int keyi = left;
//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[keyi], &a[left]);
return left;
}
2.挖坑
1.选出一个数据(一般是最左边或是最右边的)存放在key变量中,在该数据位置形成一个坑
2、还是定义一个L和一个R,L从左向右走,R从右向左走。(若在最左边挖坑,则需要R先走;若在最右边挖坑,则需要L先走)
后面的思路与hoare版本思路类似
int PartSort2(int *a,int left,int right)
{
int mid = GatMid(a, left, right);
swap(&a[left], &a[mid]);
int key = a[left];
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;
}
3.前后指针法
1、选出一个key,一般是最左边或是最右边的。
2、起始时,prev指针指向序列开头,cur指针指向prev+1。
3、若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后cur指针++;若cur指向的内容大于key,则cur指针直接++。如此进行下去,直到cur到达end位置,此时将key和++prev指针指向的内容交换即可。
经过一次单趟排序,最终也能使得key左边的数据全部都小于key,key右边的数据全部都大于key。然后也还是将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作
int PartSort3(int* a, int left, int right)
{
int mid = GatMid(a, left, right);
swap(&a[left], &a[mid]);
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if(a[cur] < a[keyi] && ++prev != cur)
{
swap(&a[prev], &a[cur]);
}
++cur;
}
swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
void QuickSort(int* a, int begin, int end)
{
if ( begin >= end)
return;
int key = PartSort3(a, begin, end);
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
void QuickSortNot(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, end);
StackPush(&st, begin);
while (!StackEmpty(&st))
{
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int keyi = PartSort3(a, left, right);
if (keyi + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
StackPush(&st, keyi - 1);
StackPush(&st, left);
}
}
}
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log^n)的时间复杂度。代价是需要额外的内存空间。归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
归并排序算法思想:分而治之:
分解:把长度为n 的待排序列分解成 两个长度为n/2 的序列
治理:对每个子序列分别调用归并排序,进行递归操作。当子序列长度为1 时,序列本身有序,停止递归
合并:合并每个排序好的子序列
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin == end)
return;
int mid = (begin + end) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
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 + begin,tmp + begin,sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
//归并排序非递归
void MergeSortNot(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail");
return;
}
int gap = 1;
while (gap < n)
{
int j = 0;
for (int i = 0;i < n;i += 2*gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
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;
}
free(tmp);
}
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
void CountSort(int* a, int n)
{
int min = a[0],max = a[0];
for (int i = 0;i < n;i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i] > max)
{
max = a[i];
}
}
int range = max - min + 1;
int* counta = (int*)malloc(sizeof(int) * range);
memset(counta, 0, sizeof(int) * range);
//统计次数
for (int i = 0;i < n;i++)
{
counta[a[i] - min]++;
}
//排序
int k = 0;
for (int j = 0;j < range;j++)
{
while (counta[j]--)
{
a[k++] = j + min;
}
}
}
稳定的排序:冒泡排序,插入排序,归并排序、计数排序
不稳定的排序:选择排序,堆排序,快速排序,希尔排序
平均时间复杂度T(n) = O(nlogn):希尔排序,归并排序,快速排序,堆排序
平均时间复杂度T(n) = O(n²):冒泡排序,简单选择排序,插入排序
最好时间复杂度T(n) = O(n):冒泡排序,插入排序
最好时间复杂度T(n) = O(nlogn):归并排序,快速排序,堆排序
最好时间复杂度T(n) = O(n²):简单选择排序
最坏时间复杂度T(n) = O(nlogn):归并排序,堆排序
最坏时间复杂度T(n) = O(n²):冒泡排序,简单选择排序,插入排序,快速排序
空间复杂度O(1):冒泡排序,简单选择排序,插入排序,希尔排序,堆排序
空间复杂度O(n):归并排序
空间复杂度O(nlogn):快速排序