文章目录
注意:以下的所有排序都以排升序为例
一、插入排序
##——直接插入排序
思想:
- 首先将数组的第一个元素看成一个有序集合,将第一个和后一个与之比较,进行排序
- 然后将前两个看做一个有序集合,后一个数再和这个有序集合中的元素进行比较
- 以此类推,将上一层循环中排好序的 i 个元素作为一个有序集合,第 i+1个元素再与这 i 个元素进行比较排序
//直接插入排序
void InsertSort(int* a, int len)
{
int i = 0;
int end = 0;//控制单趟遍历的终点
for (i; i < len - 1; i++)//将每一个元素都遍历完
{
end = i;
while (end >= 0)
{
if (a[end] > a[end + 1])
{
swap(&(a[end]), &(a[end + 1]));
end--;
}
else
{
break;
}
}
}
}
——希尔排序
——希尔排序就是对直接插入排序的优化(先对整个数组进行一次预排序,分成多个小组,然后对这些小组分别进行直接插入排序)
希尔排序的优化:
- i走到n-gap处的位置就停止,因为每次比较的是a[i]和a[i+gap]的值,走到n-gap这个位置,就可将全部数据比较完
void ShellSort(int *a, int n)
{
assert(a != NULL);
int i = 0;
int end = 0;
int gap = n / 3 + 1;
while (gap > 1)
{
for (i; i < n - gap; i++)
{
end = i;
while (end >= 0)
{
if (a[end] > a[end + gap])
{
swap(&a[end], &a[end + gap]);
end--;
}
else
{
break;
}
}
}
gap=gap / 3 + 1;//不停地对gap进行更新,缩小增量gap,使数组更加有序
}
InsertSort(a, n);
}
对比:
区别 | 选择排序 | 希尔排序 |
---|---|---|
时间复杂度 | O(n^2),最好是O(n) | O(n1.25)~O(1.6n1.25) |
空间复杂度 | O(1) | O(1) |
二、选择排序
##——直接选择排序
思想:
每次从数组中选择一个最大的(或者最小)的数,将其他的数依次与之比较,放在合适的位置上
优化:
从一个数组中分别选出最大的和最小的数,然后将最大的数放在最右边,最小的数放在最左边
再缩小范围,循环查找排序,直到数组有序为止
思路:
- 每次遍历选出最小(最大)的数,然后与最左(最右)的数进行交换
- 每选一次,范围缩小一次(left–;right++)
- 依次进行,直到只剩下一个数据
代码实现:
void SelectSort(int* a,size_t n)//选择排序
{
assert(a != NULL);
for (int i = 0; i < n; i++)
{
int min = i;
for (int j = i+1; j < n; j++)
{
if (a[min] > a[j])
{
min = j;
}
}
Swap(&a[min], &a[i]);
}
}
优化的选择排序:
采用两个指针,每次找出最小和最大的数据,分别和从最左和最右的数据进行交换
void SelectSort(int* a, int n)
{
assert(a != NULL);
int left = 0;
int right = n - 1;
int min = left;//存储最小数的下标
int max = left;//存储最大数的下标
int i = 0;
for (i; i <= right; i++)
{
if (a[min] > a[i])//找出数组中最小的数
min = i;
if (a[max] < a[i])//找出数组中最大的数
max = i;
}
swap(&a[min], &a[left]);
if (max != left)
{
swap(&a[max], &a[right]);
}
else//若最大的值在最左边
{
//此时max位置的数,经过if语句的交换,已经变成最小值
//而left位置的最大值,已经被交换到min位置
max = min;
swap(&a[max], &a[right]);
}
left++;
right--;
}
——堆排序
重点内容
堆排序:向下调整+建堆+堆排
升序——建大堆;降序——建小堆
原因:以升序为例,建大堆的话,可以得出最大的数据(堆顶),然后将堆顶的数换到堆的最后一个位置a[n-1]
下次再对a[n-1]之前的n-1的元素排序,得到第二大的元素,放于a[n-2]的位置,循环下去,直到只有一个元素为止
具体步骤:
1.先找到第一个非叶子节点,下标为(n-2)/ 2
2.找出该节点中左右孩子中较大的值,并与之交换
3.交换完后,下标减一,即找第二个根节点,再做如上操作
4.依次进行,直到走到根节点
//向下调整
void AdjustDown(int *a, int n,int root)
{
assert(a != NULL);
int parent = root;
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child++;//选出孩子中较大的那个
}
if (child + 1 < n && a[parent] < a[child])
{
//此时的child,已经是两个孩子中较大的那个
swap(&a[parent], &a[child]);
}
parent = child;
child = parent * 2 + 1;
}
}
//排升序——建大堆(堆顶为最大元素)
void MakeHeap(int *a, int n)
{
assert(a != NULL);
//这里的n代表堆中节点的个数
int i = (n - 2) >> 1;//找第一个非叶子节点
for (i; i >= 0; i--)
{
AdjustDown(a, n, i);
}
}
//堆排序
void HeapSort(int* a, int n)
{
if (a == NULL || n < 2)
{
return;
}
MakeHeap(a, n);
int end = n - 1;
while (end >= 0)
{
swap(&a[end], a[0]);
//先调整,使得倒数第二大的在堆顶位置,end再--
AdjustDown(a, end, 0);//将前end个数进行向下调整
end--;
}
}
区别 | 直接选择排序 | 堆排序 |
---|---|---|
时间复杂度 | O(n^2) | O(n^logN) |
空间复杂度 | O(1) | O(1) |
三、交换排序
##——冒泡排序
一般优化:
此种方法,优化在:循环一次,找到一个最大的放在末尾,下次再找,它的区间就会减1(由第一个倒数第二个)
以此类推,优化在减少了待找区间的大小(即内层循环的趟数)
void Bubble_Sort(int a[], int len)
{
int flag = 0;//定义一个标志,如果数组原本有序,不用再进行排序
for (int i = 0; i < len - 1; i++)
{
for (int j = 0; j < len - i - 1; j++)
{
if (a[j] > a[j + 1])
Swap(&a[j], &a[j + 1]);
flag=1;
}
}
if (flag == 0)
{
break;
}
}
void print()
{
for (int i = 0; i < len; i++)
{
printf("%d", a[i]);
}
}
更加优化:(k法)
k法更加优化在:一般优化一次区间只是减1,而k法优化一次区间可以减1(或N)
void Bubble_Sort(int a[], int len)
{
int k=len-1;
int m = 0;
for (int i = 0; i < len-1; i++)
{
int flag = 0;
for (int j = 0; j < k; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
m = j;
flag = 1;
}
if (flag == 0)
{
break;
}
}
k = m;
//不能直接 k=j,如果数组后面数据本就有序,k会前移到有序位置
而j会走到范围的尾才结束,这样没有实现优化
}
}
——快速排序
★选key的优化
快速排序的效率与key的值有很大的关系,如果key值选的好(每次刚好是中间数),那么时间复杂度也会很优O(lgn),但如果key值每次恰好是最大值或者最小值的话,这里的算法就是O(n*n)了。
我们上述例子都是选最右值为key值,若最右值刚好是最小值或者最大值,那么久很麻烦了,所以这里我们需要对key值进行优化
① 随机数法
顾名思义,就是每次随机数组中的一个元素作为key值
但是,我认为意义不大,因为随机选取也可能选到最大值或者最小值
//代码大家了解一下便可:
sand(time(0));
size_t index = rand()%(right - left + 1);
size_t key = a[index];
②三数取中法
1.
选出数组中的首元素、中间数、末尾数(这里中间数并非中位数),从这三个数中选出中位数,作为key值
2.若这三个数大小相等,那key还是最大值或者最小值,出现这种情况的概率很低
3.选出中位数,将中位数的与最右数进行交换;然后把最右数(此时已经是中位数赋给key)
int Mid(int*a, int left, int right)
{
int mid = left + ((right - left) >> 1);
if (a[left] > a[right])
{
if (a[left] < a[mid])
{
return left;
}
else if (a[right] < a[mid])
{
return mid;
}
else
{
return right;
}
}
else
{
if (a[left] < a[mid])
{
return mid;
}
else if (a[right] < a[mid])
{
return right;
}
else
{
return left;
}
}
}
1. 左右指针法
思路:
1.定义两个指针begin和end,一个指向left,一个指向right
2.两个指针同时开始遍历,保证(begin小于end),指针找比key大的,end找比key小的,然后交换二者的数据
3.begin和end继续遍历,直到(begin不小于end),交换a[begin]和a[right],
4.返回begin的下标,下一次排序,以begin位置为界,分成了begin左边和begin右边两个区间
5.递归下去,直到有序为止
代码:
int LeftRightPoint(int*a, int left, int right)//左右指针法
{
assert(a != NULL);
int begin = left;
int end = right;
int mid = Mid(a, left, right);
swap(&a[mid], &a[right]);
int key = a[right];
while (begin < end)
{
while (begin < end && a[begin] <= key)
{
++begin;
}
while (begin < end && a[end] >= key)
{
--end;
}
swap(&a[begin], &a[end]);
}
swap(&a[begin], &a[right]);
return begin;
}
2. 挖坑法
思路:
1.与左右指针不同的是,挖坑法多了一个额外的index记录下标位置
2.不再是a[begin]与a[end]进行交换,而是a[begin]=a[index],begin=index;以及a[end]=a[index],end=index
3.其他的与左右指针法类似
int Digging(int*a, int left, int right)//挖坑法
{
assert(a != NULL);
int begin = left;
int end = right;
int mid = Mid(a, left, right);
swap(&a[mid], &a[right]);
int key = a[right];
//与左右指针唯一不同的就是,挖坑法多了一个下标,来存储最右(最左)元素
int index = right;
while (begin < end)
{
while (begin < end && a[begin] <= key)
{
++begin;
}
a[index] = a[begin];
index = begin;
while (begin < end && a[end] >= key)
{
--end;
}
a[index] = a[end];
index = end;
}
a[index] = key;
return key;
}
3. 前后指针法
思路:
1.与前面两种类似,区别在于不是两个指针从两边往中间移动,而是前后两个指针 int prev=left;int post=prev-1
2.如果a[prev]小于key,post++,如果(prev!=post) swap(&a[prev,&a[post])
3.其他的与上述两种方法类似
int PrevPost(int*a, int left, int right)
{
int prev = left;
int post = prev - 1;
int mid = Mid(a, left, right);
swap(&a[mid], &a[right]);
int key = a[right];
while (prev != right)
{
if (a[prev] > key)
{
++post;
if (post != prev)
{
swap(&a[prev], &a[post]);
}
}
++prev;
}
++post;
swap(&a[prev], &a[post]);
return post;
}
快排
void QuickSort(int*a, int left, int right)
{
if (a == NULL || left >= right)
{
return;
}
int div = LeftRightPoint(a, left, right);
int div = Digging(a, left, right);
int div = PrevPost(a, left, right);
QuickSort(a, left, div - 1);
QuickSort(a, div + 1,right);
}
4. 用栈实现非递归的快排
void QuickSortNR(int* a, int left, int right)//快排,非递归
{
assert(a != NULL);
Stack s;
StackInit(&s);
StackPush(&s, left);
StackPush(&s, right);
while (StackEmpty(&s) != 0)//栈不为空
{
int end = StackTop(&s);
StackPop(&s);
int start = StackTop(&s);
StackPop(&s);
int div = GetMidNumber(a, start, end);
if (start< div-1)
{
StackPush(&s, start);
StackPush(&s, div - 1);
}
if (end > div + 1)
{
StackPush(&s, div + 1);
StackPush(&s, end);
}
}
}
四、 归并排序
思路:
1. 采用快排分治算法的思想,没有基准,直接将数组一分为二。
2. 当二分为只剩下两个或者一个元素的时候,比较大小排序。
3. 递归回溯时,一次将两个数组进行合并,并使得合并后数组有序。
//归并排序——合并的过程
void _MergeSort(int* a, int left, int mid, int right)
{
assert(a != NULL);
//tmp每一次的大小不定,根据你传参确定其大小和范围
int*tmp = (int*)malloc(sizeof(int)*(right - left + 1));
assert(tmp != NULL);
memset(tmp, 0, sizeof(int)*(right - left + 1));
int index = 0;//存放tmp中元素的下标
int begin1 = left;//定义第一个数组的范围
int end1 = mid;
int begin2 = mid + 1;//定义第二个数组的范围
int end2 = right;
//把两个数组的元素逐一比较,选出较小的放入tmp中
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//如果数组1 或 数组2 中还有元素,直接复制到tmp后
if (begin1 <= end1)
{
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
}
else
{
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
}
//用tmp数组中的元素将原数组中无序的数据替换
for (index=0; index < right - left + 1; index++)
{
//注:这里的tmp是每次的排好序的子数组,但是a的空间不变,所以将tmp的元素赋给a,是放在了a的相对位置上
a[left+index] = tmp[index];
}
free(tmp);
}
void MergeSort(int* a, int left, int right)
{
assert(a != NULL);
if (left >= right)
{
return;
}
if (right - left + 1 > 5)//小区间优化
{
int mid = left + ((right - left) >> 1);
MergeSort(a, left, mid);
MergeSort(a, mid+1, right);
_MergeSort(a, left, mid, right);
}
else
{
InsertSort(a+left, right - left + 1);
}
}
五、计数排序
计数排序算法的原理跟哈希表的K-V模型比较相似
①遍历一遍数组,得出数组的范围range,创建一个大小为range的数组,即哈希表,初始化为全0。
②再从头开始遍历数组,数字重复出现一次,在其相应的位置对应的数值加1。
③从左到右开始遍历哈希表,将数值不为0的位置的下标存储到原数组中,且数值是多少就存储多少个 。
PS:计数排序会去除重复的元素。
六、总结篇
#——各个算法稳定性、时间复杂度的对比