排序
1.插入排序
插入排序基本思想
把待排序的记录按其关键码值的大小逐个插入到一 个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
动图演示
直接插入排序特性
a:元素越接近有序,直接插入排序算法的时间效率越高
b:时间复杂度: O(N^2)
c:空间复杂度: O(1)
d:稳定性 :稳定
代码
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)//小于n-1是为了防止 最后一次end+1越界
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])//排升序 如果排降序就用>
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
以上述代码为例,每次循环运行的结果(a内元素分别为9 6 3 2 8)
2.希尔排序
希尔排序基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量(间距)逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,排序便终止。
希尔排序特性
a:序是对直接插入排序的优化
b:当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样排序就会得到优化
c:稳定性: 不稳定
d:时间复杂度 O(n^1.3)
void ShellSort(int* a, int n)
{
int gap = n ;
while (gap > 1)
{
gap /= 2;
//或者是 gap=gap/3+1;这个更好
for (int i = 0; i < n - gap; i++)
{
int end = i;
// i<n-gap 是为了防止 end+gap的越界
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;
}
}
}
上述代码难以理解,我们来挨个挨个解决
我们首先来看gap不变的情况下是什么样子的
假设gap=3,代码就变成了
int gap = 3;
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;
}
还是有点不好理解那我们就可以再改变一下
int gap = 3;
for (int j = 0; j < gap; j++)
{
for (int i = j; i < n - gap; i+=gap)
{
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;
}
}
这里 结合给的图理解 j控制的循环的作用就是控制了 蓝 红 绿 三组都能进行预排序 而i控制的循环是对每组分别进行排序 也就是说第三段代码表达的意思就是 “分好组之后再挨着对每组进行排序”
有了对第三段代码的初步理解之后,我们再来看第二段代码i虽然每次加1 但是它还是间距为gap的关键字进行比较 那么第二段代码表达的意思就是 “边分组边比较” 也就是 “大乱炖”
需要注意的是 两段代码并没有效率上的区别, 第三段代码 并不会因为三层循环效率就低
用不同的gap值进行排序,都会让数据更加接近有序, 第一段代码表达的就是这个意思 ,而为什么每次都除以二呢 ,是为了保证最后一次一定是1 当gap是1的时候就是直接插入排序 通过预排序 数据已经基本有序 所以此时直接插入排序的效率会大大提高
gap的意义
a:gap越大, 大的数可以更快的到后面, 小数更快到前面 但是越不接近有序
b:gap越小, 数据跳动的也就越慢, 越接近有序
3.选择排序
选择排序基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
既然每次循环我们已经选择除了最大或最小的数据,那不如同时把两者记录下来
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
//用来记录下标的位置
int min = begin;
int max = begin;
for (int i = begin + 1; i <= end; i++)
{
if (a[min] > a[i])
{
min = i;
}
if (a[max] < a[i])
{
max = i;
}
}
swap(&a[min], &a[begin]);
//这里多了一个判断是因为 max最大值刚好在begin的位置上,交换后max去了min的位置上去 所以要判断一下
if (begin == max)
max = min;
swap(&a[max], &a[end]);
begin++;
end--;
}
}
选择排序特性
a:时间复杂度 O(n^2)
b:空间复杂度 O(1)
c:稳定性 :不稳定
选择排序的时间复杂度不同于 直接插入排序 选择排序的时间复杂度并不会因为数据有序而降低 所以我们很少用到这个选择排序
而直接插入排序 对于局部有序 效率都会提升。
4 堆排序
堆排序基本思想
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储
在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为
小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
代码
void AdjustDown(int* a,int n,int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
child++;
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
void HeapSort(int *a , int n)
{
for (int i = (n-2) / 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--;
}
}
堆排序分为两个部分: 建堆和排序重新建堆
我们要想理解堆排序算法就要先理解什么是向下调整和向上调整
既然向上,向下调整都能建堆,我们为什么要用向下调整的方法呢 ,因为向下调整建堆时间复杂度要优于向上调整
证明
同理向上调整的时间复杂度为
第二步排序重新建堆是指 堆顶的元素先和堆的最后一个元素进行交换,然后新的堆顶的元素再进行向下调整,这是对于逻辑结构来说的
我们的操作都是在数组即物理结构中 我们以排升序建大堆为例:数组第一个元素a[0]此时是最大的,和数组中最后一个元素进行交换 那么此时数组最后一个元素就是最大的 然后对堆顶元素向下调整 (这时候因为数组最后一个元素已经排好了,所以在调整的时候我们要把他 “去掉”) ,这时堆顶元素就是次大的 ,再和倒数第二个元素进行交换 , 以此类推,这就是排序重新调整的过程
堆排序特性
a: 时间复杂度:O(N*logN)
b: 空间复杂度:O(1)
c: 稳定性:不稳定
5.冒泡排序
代码
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; ++j)
{
int flag = 0;
for (int i = 1; i < n-j; ++i)
{
if (a[i - 1] > a[i])
{
swap(&a[i - 1], &a[i]);
flag = 1;
}
}
if (flag == 0)
{
break;
}
}
}
冒泡排序特性
a:时间复杂度:O(N^2)
b:空间复杂度:O(1)
c:稳定性:稳定
6.快速排序
快速排序基本思想
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
时间复杂度:O(N*logN)
空间复杂度:O(logN)
hoare版本
代码
void QuickSort(int* a, int left ,int right)
{
int l = left;
int r = right;
int key = a[left];
if (left > right)
return;
while (l < r)
{
while (l < r && a[r] >= key)
r--;
while (l < r && a[l] <= key)
l++;
swap(&a[l], &a[r]);
}
swap(&a[left], &a[r]);
QuickSort(a, left, r - 1);
QuickSort(a, r + 1, right);
}
上述过程是一个循环,循环 结束后除了保证key左边的值都比右边的值小之外,key还到了它正确的位置,即key已经排好了,那我们就需要通过递归,再去把key左右两边的值进行排序
需要注意的是
1.选取左边元素做key,需右边先走(这样保证了相遇位置比key小),右边做key,左边先走(保证了相遇位置比key要大)
2.等号不能省略(至少要存在一个等号,但是最好是两个都写上),两个都省略会导致死循环
对于快速排序的时间复杂度我们可以粗略的计算一下
针对上述数据有序的情况下我们就可以采取三数取中的优化方案。三数取中指的是在begin,mid,end位置上取出中间位置的值,取完之后我们把这个值和我们原先所选的key的位置进行交换,这样就不用再进行其他的修改了
int Getmid(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[mid] < a[end])
{
if (a[begin] < a[mid])
return mid;
else if (a[end] < a[begin])
return end;
else
return begin;
}
else //a[mid] > a[end]
{
if (a[begin] > a[mid])
return mid;
else if (a[end] > a[begin])
return end;
else
return begin;
}
}
void QuickSort(int* a, int left ,int right)
{
if (left > right)
return;
int mid = Getmid(a, left, right);
//swap(&a[left], &a[mid]);
int l = left;
int r = right;
int key = a[left];
while (l < r)
{
while (l < r && a[r] >= key)
r--;
while (l < r && a[l] <= key)
l++;
swap(&a[l], &a[r]);
}
swap(&a[left], &a[r]);
QuickSort(a, left, r - 1);
QuickSort(a, r + 1, right);
}
除了上述问题之外,我们还会遇到另一个问题,这是递归版本,我们在排最后几个数的时候
这就用到了小区间优化
我们用小区间优化理论上可以节省掉80%以上的空间,因为
void QuickSort(int* a, int left ,int right)
{
if (left > right)
return;
if ((right - left + 1) < 10)
{
InsertSort(a + left, right - left + 1);//注意此时的区间应该是从a+left位置开始的,因为这是被递归分割的区间
//不能从头开始
}
else
{
int mid = Getmid(a, left, right);
//swap(&a[left], &a[mid]);
int l = left;
int r = right;
int key = a[left];
while (l < r)
{
while (l < r && a[r] >= key)
r--;
while (l < r && a[l] <= key)
l++;
swap(&a[l], &a[r]);
}
swap(&a[left], &a[r]);
QuickSort(a, left, r - 1);
QuickSort(a, r + 1, right);
}
}
2.挖坑法
代码
void QuickSort(int* a, int left, int right)
{
if (left > right)
return;
if ((right - left + 1) < 15)
{
InsertSort(a + left, right - left + 1);
}
else
{
int mid = Getmid(a, left, right);
swap(&a[left], &a[mid]);
int l = left;
int r = right;
int key = a[left];
int hole = left;
while (l < r)
{
while (l < r && a[r] >= key)
{
r--;
}
a[hole] = a[r];
hole = r;
while (l < r && a[l] <= key)
{
l++;
}
a[hole] = a[l];
hole = l;
}
a[hole] = key;
QuickSort(a, left, r - 1);
QuickSort(a, r + 1, right);
}
}
我们拿一个循环过程来看与hoare版本的区别
3.前后指针法
代码
void QuickSort(int* a, int left, int right)
{
if (left > right)
return;
if ((right - left + 1) < 15)
{
InsertSort(a + left, right - left + 1);
}
else
{
int mid = Getmid(a, left, right);
swap(&a[left], &a[mid]);
int cur = left + 1;
int prev = left;
int key = left;
while (cur <= right)
{
if (a[cur] < a[key] && ++prev != cur)//++prev!=cur是因为当两者重合的时候也不用交换了,提高效率
{
swap(&a[cur], &a[prev]);
}
++cur;
}
swap(&a[left], &a[prev]);
QuickSort(a, left, prev - 1);
QuickSort(a, prev + 1, right);
}
}
前后指针法的精髓就在于cur找比key所在位置值小的位置,然后停下来,接着++prev 然后交换两者的值
通过一个动图来演示
4.非递归版
非递归版也是根据递归版的思路来的,只不过是用栈来模拟递归的过程,像二叉树的深度遍历
我们知道通过递归,我们是分割的一个个区间, 所以我们用栈存储的应该是区间的值, 并且我们是从左到右递归的,那么根据栈的性质,存储数据的时候也应该是右侧区间先入栈
代码
void QuickSort(int* a, int begin, int end)
{
stack<int> s;
s.push(begin);
s.push(end);
while (!s.empty())
{
int right = s.top();
s.pop();
int left = s.top();
s.pop();
int mid = GetMidIndex(a, begin, end);
swap(a[right], a[mid]);
//中间的这段代码是可以替换成上面三种方法的任何一种
int keyi = left;
int prev = left, 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;
//
if (keyi + 1 < right)
{
s.push(keyi + 1);
s.push(right);
}
if (left < keyi - 1)
{
s.push(left);
s.push(keyi - 1);
}
}
}
5.三路划分
经典的快速排序算法都是二路划分,所谓二路划分指的就是每趟排序让比key值大的到一边,小的到另一边,这样的算法经过三数取中等各类优化之后,能够适应大部分数据,但是对于有很多重复元素的情况,它的时间复杂度又会退化成O ( N ^ 2) (因为在包含大量重复元素的情况下,相当部分的区间,会存在重复元素做key值,那么下次区间划分的时候,就会导致下个区间里面仅有一个值(就是key),剩下的元素都在另一个区间,这样我们就又回到了O ( N ^2))
为了解决两路划分的不足,就提出了三路划分的算法,三路划分就是小于key的放到一起,大于key的放到一起,等于key的放到一起.
最关键的思想就是
直接来看个动图理解思想
很多同学都会有个疑问,为什么第三种情况cur不用++,这是因为交换位置之后cur当前的元素还是可能比key要大,如果此时把cur++,就忽略掉了这个元素,出现错误
来看代码是怎么实现的
void QuickSortTD(int* a, int begin, int end)
{
if (begin >= end)
return;
int mid = Getmid(a, begin, end);
swap(&a[begin], &a[mid]);
int l = begin;
int r = end;
int key = a[l];
int cur = begin + 1;
while (cur <= r)
{
if (a[cur] < key)
{
swap(&a[cur], &a[l]);
cur++;
l++;
}
else if (a[cur] > key)
{
swap(&a[cur], &a[r]);
r--;
}
else
{
cur++;
}
}
QuickSortTD(a, begin, l - 1);
QuickSortTD(a, r + 1, end);
}
7.归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
再借助动图理解一下
递归版
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 k = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[k++] = a[begin1++];
}
else
{
tmp[k++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[k++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[k++] = 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)
{
perror("malloc fail");
exit(-1);
}
_MergeSort(a, 0, n - 1 , tmp);
free(tmp);
tmp = NULL;
}
归并排序就是像上面的图一样,把每一段区间里的值排好序之后,再跟其他的区间进行合并,这样不断地合并,到最后整段区间就是有序的了
时间复杂度:O(N*logN)
空间复杂度:O(N)
实际上因为归并也递归了,空间复杂度应该是O(N+logN)
但是因为logN和N相差很大,就把logN忽略掉了
非递归版
递归版的归并排序,无非就是区间划分是通过递归来实现的,那我们通过循环来实现的话,就需要考虑怎样把区间划分好
代码
void MergeSortNR(int *a,int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
int N = 1;
while (N < n)
{
for (int i = 0; i < n; i += 2 * N)
{
int begin1 = i;
int end1 = i + N - 1;
int begin2 = i + N;
int end2 = i + 2 * N - 1;
int j = i;
if (end1 >= n)//判断end1,begin2,end2是否越界
{
end1 = n - 1;
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)//判断begin2,end2是否越界
{
begin2 = n;
end2 = n - 1;
}
else if (end2 >= n)//判断end2是否越界
{
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, tmp, sizeof(int) * n);
N *= 2;
}
free(tmp);
tmp = NULL;
}
现在来解释一下上面写法的原理和注意事项
void MergeSortNR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
int N = 1;
while (N < n)
{
for (int i = 0; i < n; i += 2 * N)
{
int begin1 = i;
int end1 = i + N - 1;
int begin2 = i + N;
int end2 = i + 2 * N - 1;
int j = i;
if (end1 >= n)//判断end1,begin2,end2是否越界
{
break;
}
else if (begin2 >= n)//判断begin2,end2是否越界
{
break;
}
else if (end2 >= n)//判断end2是否越界
{
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));
}
N *= 2;
}
free(tmp);
tmp = NULL;
}
归并排序特性
1.时间复杂度:O(N*logN)
2.空间复杂度:O(N)
3.稳定性:稳定
总结
稳定性是指 在排序过程中保持相同元素的相对位置保持不变