七种排序
从大类上可以分为插入排序、选择排序、交换排序和归并排序
插入排序: 插入排序、希尔排序
选择排序:选择排序、堆排序
交换排序:冒泡排序、快速排序
归并排序: 归并排序
1、插入排序
插入排序通过将一个元素插入一个按照大小排序好的序列这一过程,来实现对所有元素的排序。
时间复杂度:O(N^2)
插入时的两种情况:
1)找到了比tmp小的元素下标end,然后将tmp插入end下标的后面
2)直到end<0时,说明tmp是最小的元素,则将tmp插入第一个位置
代码实现:
//插入排序
template<class T>
void InsertSort(T* a,int n)
{
for (int i = 0; i < n-1; ++i)
{
int end = i;
int tmp = a[end+1];
for (end = i; end >= 0; --end)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
2、希尔排序
对一组数据进行多次分组(gap为每组相邻两元素的间距)预排序,然后再进行一次插入排序。希尔排序是对插入排序的优化。
原理:是通过预排序将小的元素尽可能较快的移至前面。最后通过 一次插入排序最终整合
时间复杂度:O(N)~O(N^2)
问:如何确定gap的值?
刚开始定义gap=n即等于元素的个数,
单趟预排序通过gap=gap/3+1;来决定每次循环gap的值
gap>1时进行预排序,gap==1时进行插入排序整合。
注意:预排序不是将每组分开排序,而是将所有组穿插着进行排序
代码实现:
//希尔排序
void ShellSort(int *a, int n)
{
int gap = n;
while (gap > 1) //进行多次预排序
{
gap = gap / 3 + 1;
//gap>1时进行预排序 gap==1时进行最后一次插入排序
//预排序多组间交替排序(即一次循环可以排多组)
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
for (end = i; end >= 0; end -= gap)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
3、选择排序
选择排序是每次从某个集合里选出最大或最小的一个元素,每次可以确定一个元素的位置,将集合不断缩小,最终完成对整个集合的排序。
时间复杂度:O(N^2)
优化版:每次可以取出最大值和最小值,确定两个位置,提高一倍效率
注意:
优化版本可能会出现记录最大值的下标(max)在集合最左端(begin),记录最小值的下标(min)在集合最右端(end),出现交换两次后又将两元素交换回去的情况
解决方法:
当将min下标的元素和begin下标的元素进行交换后,将max变量更新为min,加一条判断即可(详情见代码实现)
代码实现:
//选择排序
//优化版选择排序---每次选择两个(最大和最小)
void SelectSort(int *a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int max = begin;
int min = begin;
for (int i = begin; i <= end; ++i)
{
if (a[i] > a[max])
{
max = i;
}
if (a[i] < a[min])
{
min = i;
}
}
swap(a[min], a[begin]);
if (max == begin) //解决max==begin且min==end造成交换两次的情况
{
max = min;
}
swap(a[max], a[end]);
begin++;
end--;
}
}
4、堆排序
通过建堆和堆算法来实现多一个集合的排序
时间复杂度:O(N*lgN)
堆排序过程:
1、建大堆
2、将堆顶元素和堆的最后一个元素交换
3、堆顶向下调整
4、将最后一个元素去除在堆外(但不是真正的删除)
代码实现:
void AdjustDown(int *a, int root, int end) //向下调整
{
int parent = root;
int child = parent * 2 + 1;
while (child <= end)
{
if (a[child] < a[child + 1] && (child + 1) <= end)
{
++child;
}
if (a[parent] >= a[child])
{
break;
}
swap(a[parent], a[child]);
parent = child;
child = parent * 2 + 1;
}
}
void HeapSort(int *a, int n)
{
int end = n-1; //end是数组最后一个元素下标
//建大堆
for (int i = (end - 1) / 2; i >= 0; --i)
{
AdjustDown(a,i,end);
}
//交换元素和调整
while (end)
{
swap(a[0],a[end]);
--end; //每次将交换后最大的元素隐藏
AdjustDown(a,0,end);
}
}
5、冒泡排序
通过相邻的元素进行交换排序,每次循环可以确定一个最大或最小的元素,然后缩小集合范围,直至集合有序。
优化:可以增加一个标志位flag,当序列已经有序时不用再进行多余的比较
时间复杂度:O(N^2)
代码实现:
//冒泡排序
void BubbleSort(int *a,int n)
{
for (int i = n-1; i > 0; --i)
{
bool flag = 0; //加标志位提高效率(已经有序时直接退出)
for (int j = 0; j < i; ++j)
{
if (a[j + 1] < a[j])
{
swap(a[j + 1], a[j]);
flag = 1;
}
}
if (flag == 0) //表示上次一单趟比较没有进行交换--可以直接结束排序
{
break;
}
}
}
6、快速排序
找出一个键值key,将集合中小于key的值放一边,大于key的值放在另外一边,即一次排序可以确定key值的准确位置,然后递归排序key的左边和key的右边,直至集合有序。
时间复杂度:O(N*lgN)
空间复杂度:O(lgN)
快排的时间复杂度最坏情况的O(N^2),但快排可以通过各种优化保证时间复杂度不会到最坏,所以认为快排的时间复杂度为O(N*lgN)。
快排的单趟排序方法:
1)左右指针法、挖坑法,两种方法原理基本一致(见代码)。
2)前后指针法:通过两个指针走的快慢不同将单趟集合元素分出大于key和小于key两个区间,具体实现见代码。
快排在时间复杂度上的优化:
1)三数取中法:对key值的选取一般为最右端的值,但最右端得值若是最大或最小值,则此次单趟排序没意义,故取最左端、中间、最右端三个值当中的中间大小的值与最右端值进行交换,可以保证每趟排序不会出现没有元素交换的现象。
2)小区间优化法:当快排递归到比较小的区间进行排序时,可选用插入排序等排序方式代替快排,减小空间和调用函数栈帧时间上的开销。
非递归实现快排:
用栈结构模拟递归的调用过程实现,每次单趟排序从栈里取需要排序的区间,并将单趟排序得到的两个小区间分别入栈,等待下次出栈排序,直至栈为空排序结束
代码实现:
//快速排序
//[left,right]闭区间
int PartSort(int *a, int left, int right) //单趟排序
{
//int key = a[right]; //挖坑法
int key = right; //左右指针法
while (left < right)
{
while (left < right && a[left] <= a[key])
{
left++;
}
//a[right] = a[left];
while (left < right && a[right] >= a[key])
{
right--;
}
//a[left] = a[right];
swap(a[left],a[right]);
}
swap(a[left], a[key]);
//a[left] = key;
return left;
}
int PartSort2(int *a, int begin, int end) //前后指针法
{
int cur = begin;
int prev = cur - 1;
while (cur < end)
{
if (a[cur] < a[end] && ++prev != cur)
{
swap(a[cur], a[prev]);
}
cur++;
}
swap(a[++prev], a[end]);
return prev;
}
void QuickSort(int *a, int begin, int end)
{
if (begin >= end)
{
return;
}
if (end - begin < 5) //优化---对深层次递归优化为其他排序方式(减少空间开销)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int mid = PartSort2(a, begin, end);
QuickSort(a, begin, mid - 1);
QuickSort(a, mid + 1, end);
}
}
//非递归快排
void QuickSortNonR(int *a, int begin, int end)
{
stack<int> s;
s.push(end);
s.push(begin);
while (!s.empty())
{
int left = s.top();
s.pop();
int right = s.top();
s.pop();
int mid = PartSort(a, left, right);
if (left < mid - 1) //当区间只有一个元素进行排序时不用入栈
{
s.push(mid - 1);
s.push(left);
}
if (mid + 1 < right)
{
s.push(right);
s.push(mid + 1);
}
}
}
7、归并排序
将两段有序区间归并为一段有序的区间,但归并排序在空间上会有消耗(开辟同等大小的缓冲区),更适合外排序,
时间复杂度:O(N*lgN)
空间复杂度:O(N)
代码实现:
//归并排序
void _Merge(int *a, int *tmp, int begin, int mid_left, int mid_right, int end)
{
int tmp_begin = begin;
int left_begin = begin;
int left_end = mid_left;
int right_begin = mid_right;
int right_end = end;
if (left_begin >= right_end)
return;
while (left_begin <= left_end && right_begin <= right_end)
{
if (a[left_begin] > a[right_begin])
{
tmp[tmp_begin++] = a[right_begin++];
}
else
{
tmp[tmp_begin++] = a[left_begin++];
}
}
if (left_begin > left_end)
{
memcpy(tmp + tmp_begin, a + right_begin, sizeof(a[0]) * (right_end - right_begin + 1));
}
else
{
memcpy(tmp + tmp_begin, a + left_begin, sizeof(a[0]) * (left_end - left_begin + 1));
}
memcpy(a + begin, tmp + begin, sizeof(a[0]) * (end - begin + 1));
}
void MergeSort(int *a, int *tmp, int begin, int end)
{
if (begin >= end)
{
return;
}
int mid = begin + ((end - begin) >> 1);
MergeSort(a, tmp, begin, mid);
MergeSort(a, tmp, mid + 1, end);
_Merge(a, tmp, begin, mid, mid + 1, end);
}
对各种排序的比较:
时间复杂度都为N^2的三个排序里,效率高低的相对排序(最好情况)
插入排序 > 冒泡排序 > 选择排序
稳定的排序(三种):
插入排序、冒泡排序、归并排序