选择题考点:
(1)堆排序,快速排序,冒泡排序每趟都会把一个值放在正确的位置上,但是希尔排序不会!
(2)归并排序和快速排序各自的优缺点:
归并排序比快速排序稳定,并且归并排序最差情况是O(nlogn),而快速排序最差情况是O(n^2)。归并排序空间换时间,快速排序不要耗空间。
在学习算法的时候,一定要搞清楚每一个排序算法的适用情况,每个排序算法都有自身的适用特点,记住他们的优势,这样可以针对性的使用以及在面试的时候和面试官探讨。
对于近乎有序的数组,插入排序的效率非常高,时间复杂度最高可缩小到O(n)
第一种最常用的就是
1.选择排序:
时间复杂度:O(N*N)
空间复杂度:O(1)
大致思想为:从第一个数开始,依次从后面所有数中挑出最小的那个数(包括比较第一个数),然后与第一个数进行数值交换。
自己写代码总会出现或多或少的小问题:
注意:
(1)swap函数记得加上头文件#include
(2)自己写交换函数记得用引用传递,int temp = a;a=b;
b=temp;这个交换顺序自己在纸上写一写。
(3)为了可以容纳多种类型数组,加入模板T,同时包装成一个函数,这样float型数组(小数),string型数组(字符串)都可以进行排序。
#include<iostream>
using namespace std;
#include<algorithm>
//void swap(int& a, int& b)
//{
// int temp = a;
// a=b;
// b=temp;
//}
template<typename T>
void selectSort(T arr[], int n){
for(int i=0; i<n; i++)
{
int min = i;
for(int j=i+1; j<n; j++)
{
if(arr[j]<arr[min])
min = j;
}
swap(arr[i],arr[min]);
}
}
int main(){
int n=6;
int arr[6]={
5,2,4,3,6,1};
selectSort(arr,6);
for(int k=0; k<n; k++)
{
//cout<<"排序后:"<<endl;
cout<<arr[k]<<" ";
}
while(1);
return 0;
}
适用范围:若n较小(如n≤50),可采用直接插入或直接选择排序。
下面这小部分可以不用看。
后来专门搞了一个随机生成数组值得函数,放在SorttestHelper.h文件里,代码如下。
这里学习到得一个点是它得返回类型是namespace,根据这个代码可知,该返回类型是一个指向新开辟的数组空间的指针。,但事实上它返回的数组空间,只是用指针表示一下。为什么这么说呢,int * arr = 该函数, 也就是用一个指针指向该函数返回的数组空间!注意在最后用delete[] arr; 释放一下那个函数开辟的数组空间。
#include<iostream>
#include<ctime>
#include<cassert>
using namespace std;
namespace SortTestHelper {
// 生成有n个元素的随机数组,每个元素的随机范围为[rangeL, rangeR]
int *generateRandomArray(int n, int range_l, int range_r) {
int *arr = new int[n];
srand(time(NULL));
for (int i = 0; i < n; i++)
arr[i] = rand() % (range_r - range_l + 1) + range_l;
return arr;
}
2.插入排序:
时间复杂度:O(N*N)
空间复杂度:O(1)
选择排序的思想是从该数后面找到最小与该数进行交换,而插入排序的思想是,从第二个开始,把该数放到前面所有数的数组中对应的位置。(一个是把后面整体看成数组,一个是把前面整体看成一个数组)
template<typename T>
void InsertSort(T arr[], int n){
for(int i =1; i<n; i++){
for(int j=i;j>0; j--){
if(arr[j]<arr[j-1])//!这里是关键,j和j-1比较!
swap(arr[j],arr[j-1]);
else
break;
}
}
}
再补充一个比较:这个插入排序是j和j-1进行比较,而选择排序是j和保存的最小值进行比较!
适用范围:
若n较小(如n≤50),可采用直接插入或直接选择排序。
注意:插入排序如果第一轮该数比前面的数组的第一个比较的数大,那么后面就不用再继续了。所以相对选择排序还说应该是更快一点!但是事实上,是可能快也可能慢,因为有时候插入排序相比选择排序需要交换更多次!所以下面出现了插入排序的优化!
但是插入排序最大的优点在于,对于接近有序的序列,插入排序可以实现效率非常的高,甚至比很多时间复杂度小的排序方法都高,而现实生活中很多事物的排序都属于接近有序的排序,比如不小心被打乱的时间,或者注册表等!所以其实用价值很高!可以降到o(n)的复杂度。
插入排序优化:
观察下面的程序,改进后的方法:就是减少交换,把交换都换成了赋值。怎么个赋值法?
把当前的数保存在一个临时变量中(对应T e = arr[i];),然后将前面的数组和临时变量比,要是临时变量小(或者说该数小),暂时先不和之前一样让他们交换,而是就把较大的数赋值给后面的小数,一直到前面数组中哪个数比该数小,好,跳出来,把该数放在数组中这个数后面!
临时变量e中保存着最小的数,然后在循环中找到最小数的位置,然后把临时变量放进去。
template<typename T>
void InsertSort(T arr[], int n){
for(int i =1; i<n; i++){
T e = arr[i];
int j;
for( j=i;j>0 && e <arr[j-1]; j--){
arr[j]=arr[j-1];
}
arr[j] = e;
}
}
最后再强调一下:
插入排序最大的优点在于,对于接近有序的序列,插入排序可以实现效率非常的高,甚至比很多时间复杂度小的排序方法都高,而现实生活中很多事物的排序都属于接近有序的排序,比如不小心被打乱的时间,或者注册表等!所以其实用价值很高!
3.希尔排序(难)
自我感觉希尔排序的原理很好理解,就是把序列分组之后再进行插入排序,没有看过的可以看看这篇希尔排序
难就难在代码的分析上:
注意下面是三层循环,里面两层循环是模拟插入排序
下面是相对插入排序几个改动的地方,我依次解释:
template<typename T>
void Shell_sort(T arr[], int n){
for(int gap = n/2; gap>0; gap/=2)//改动
{
//改动i=gap
for(int i =gap; i<n; i++){
//e里面保存的是最小值
T e = arr[i];
int j;
//改动j>gap,j<gap时就结束
//改动e <arr[j-gap]
//改动j-=gap
//改动j-=gap
for( j=i;j-gap>=0 && e <arr[j-gap]; j-=gap){
arr[j]=arr[j-gap];
}
arr[j] = e;
}
}
}
第一个改动就是加入了gap循环,这个gap既代表有几个分组,又代表分组后每个元素之间的间隔。它循环起来是因为希尔排序要进行多次分组,每次分组都不同。直到gap=0就不用分了,这时候就排好了。
第二次改动把插入排序的初始值换了,以前是从第2个数开始作为要插入的对象,往前面的数组插入,现在是先把第gap个数作为插入对象,这个gap在它现在的分组中其实就是第2个数!
第三个改动j-gap>=0,它就相当于之前插入排序的j-1>=0,这个不用理解,只要知道数组下标不能为负数,所以j-1≥0,所以j-gap>=0。
另外两个比较简单就不说了就不用说了。
注意:分组之后的排序是交错排序,不是先把其中一组排完再排另一组!!!是按照顺序交错排序!
时间复杂度:O(n^(1.3—2))
时间复杂度相对选择排序减小了一点,但是也没减小多少。主要优点在于可以处理大型数组。
- 最好情况:O(n^1.3)
- 最坏情况:O(n^2)
空间复杂度:O(1)
适用场景
希尔排序是对直接插入排序的一种优化,可以用于大型的数组,希尔排序比插入排序和选择排序要快的多,并且数组越大,优势越大。
4.冒泡排序(史上最菜的排序算法)
时间复杂度:O(N*N)
空间复杂度为 O(1)
适用于数据量很小的排序场景。
这个比较简单,就从第一个开始,与它相邻的比较,要是偏大就交换,这样一直交换下去,最后面的那个就得到了最大值。第一轮交换最后面那个值排好,第二轮倒数后两个值排好,既然每次倒着数都增加一个排好的值,那就每一轮减少一个值的排序,只用排序前n-i个。
template<typename T>
void maopao_sort(T arr[], int n){
for(int i=0;i<n-1;i++){
for(int j=0;j<n-1-i; j++){
if(arr[j]>arr[j+1])
swap(arr[j