1.概述
1)算法分类
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 归并排序
- 快速排序
- 计数排序
- 基数排序
2) 算法复杂度
3)概念介绍
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
2.算法
1)冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.1算法原理
-
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
-
对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
-
针对所有的元素重复以上的步骤,除了最后一个。
-
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
1.2举例
以4,2,3,1为例,第一次循环4与2比较,交换4,2位置,变为2,4,3,1。4再与3比较,交换4,3,位置,变为2,3,4,1。4再与1比较,变为2,3,1,4。这样一次循环过程就结束了,当前数组中的最大值被移动到了数组最后,下一次循环时就不需要比较最后一个元素了.
1.3代码
void bubbleSort(int*array,int num){
for(int a=0;a<num-1;a++){
for(int b=0;b<num-1-a;b++){
if(array[b]>array[b+1])
swap(array[b],array[b+1]);
}
}
}
//交换数据
void swap(int&a,int &b){
int temp=a;
a=b;
b=temp;
}
1.4思考
根据上面的算法,当给一组已经有序的数据排序会怎么样呢?显然该算法还是会重新检索所有数据,这就引出了及时终止的冒泡排序,如果有一次冒泡过程数据没有发生交换的话,说明数据已经是有序的,即不需要再排了
1.5改进后的代码(及时终止的冒泡排序)
//及时终止的冒泡排序
void bubbleSortSmarter(int*array,int num){
for(int a=0;a<num-1&&bubble(array,num-1-a);a++);
}
//一次冒泡过程 若返回true说明发生了数据交换,冒泡排序应该继续进行
bool bubble(int *array,int n){
bool flag=false;
for(int i=0;i<n;i++)
if(array[i]>array[i+1]){
swap(array[i],array[i+1]);
flag=true;
}
return flag;
}
2)选择排序(Selection-sort)
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
2.1算法原理
n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:
①初始状态:无序区为R[1..n],有序区为空。
②第1趟排序
在无序区R[1..n]中选出关键字最小的记录R[k],将它与无序区的第1个记录R[1]交换,使R[1..1]和R[2..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
……
③第i趟排序
第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区
2.2举例
依旧以4,2,1,3为例,第一趟排序找出最大元素4,将其与数组最后一个未排序数字3交换,原序列变为3,2,1,4,第二趟找出未排序的最大元素3,与数组最后一个未排序数字1交换,变为1,2,3,4,第三趟再找出未排序的最大元素2,与数组 最后一个未排序数字2交换,为1,2,3,4.排序结束
2.3代码
//找出数组中最大元素
int maxOfArray(int *array,int n){
int max=n-1;
for(int i=n-2;i>=0;i--){
if(array[max]<array[i])
max=i;
}
return max;
}
//选择排序
void selectionSort(int*array,int num){
for(int i=0;i<num;i++){
swap(array[num-1-i],array[maxOfArray(array,num-i)]);
}
}
2.4思考
其实上述算法与冒泡排序一样,对有序数组不友好
2.5改进代码(及时终止的选择排序)
//找出数组中最大元素
int maxOfArray(int *array,int n){
int max=n-1;
for(int i=n-2;i>=0;i--){
if(array[max]<array[i])
max=i;
}
cout<<"!"<<max<<endl;
return max;
}
//选择排序
void selectionSort(int*array,int num){
for(int i=0;i<num&&maxOfArray(array,num-i)!=num-i-1;i++){//判断最大元素是否已处于最后
swap(array[num-1-i],array[maxOfArray(array,num-i)]);
}
}
3)插入排序(Insertion-Sort)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
3.1算法原理
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
3.2举例
对于4,2,1,3。刚开始将4看成有序,将2,1,3插入,插入2,有序数组变为2,4。插入1,有序数组变为1,2,4。插入3,有序数组变为1,2,3,4.结束
3.3代码
//插入排序
void insertSort(int*array,int num){
for(int i=1;i<num;i++){
int key=array[i];//要插入的元素
int end;
//寻找插入的合适位置
for(end=i-1;end>=0&&array[end]>key;end--)
array[(end+1)]=array[end];
//条件之一被破坏
array[end+1]=key;
}
}
3.3算法分析
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
4)希尔排序(Shell Sort)
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
4.1算法原理
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.2举例
假设待排序数据有10个,他们分别是:
49,38,65,97,76,13,27,49,55,04。
增量序列的取值依次为:
5,2,1
4.3代码
//希尔排序专属插入排序
void insertSort(int*array,int num,int start,int increment){
for(int i=start+increment;i<num;i+=increment){
int key=array[i];//要插入的元素
int end;
for(end=i-increment;end>=0&&key<array[end];end-=increment)
array[end+increment]=array[end];
array[end+increment]=array[end];
}
}
//希尔排序
void shellSort(int *array,int num){
int crement=num/2;//希尔排序的初始增量
for(;crement>=1;crement/=2){
//crement既为增量也为分成的组数
for(int i=0;i<crement;i++){
//对每一组使用一次插入排序
insertSort(array,num,i,crement);
}
}
}
4.4算法分析
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。
5)归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
5.1算法原理
第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列
5.2举例
对于5,3,4,1。首先将其分为2组,分别为{5,3},{4,1},对于{5,3},再将其分为2组,分别为{5},{3},合并后为{3,5},同理,{4,1}分组合并后为{1,4},最后再将{3,5},{1,4}合并,为{1,3,4,5}。结束
5.3代码
//归并排序
int* mergeSort(int*array,int num){
if(num<=1)
return array;
int mid=num/2;
int *left=array;
int *right=array+mid;
return merge(mergeSort(left,mid),mid,mergeSort(right,num-mid),num-mid);
}
//将两个数组合并 leftLen->left数组的长度 rightlen->right数组的长度
int* merge(int*left,int leftLen,int* right,int rightLen){
int*result=new int[leftLen+rightLen];
int leftCount=0,rightCount=0,i;//计数器
for(i=0;leftCount<leftLen&&rightCount<rightLen;i++){
if(left[leftCount]>right[rightCount]){
result[i]=right[rightCount++];
}else{
result[i]=left[leftCount++];
}
}
//条件被破坏
if(leftCount==leftLen){
for(;rightCount<rightLen;i++)
result[i]=right[rightCount++];
}else{
for(;leftCount<leftLen;i++)
result[i]=left[leftCount++];
}
return result;
}
5.4算法分析
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
6)快速排序(Quick Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
6.1算法原理
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
6.2举例
对于数组a{5,2,3,4,6}。先取一个基准值k=5,i=0,j=4(数组最后一个元素下标),先从数组右边开始一次循环,此时找的是比k小的元素并交换,a[4]>k,不交换,j-=1,a[3]<k,交换位置,交换后数组a变为a{4,2,3,5,6},此时找的是比k大的元素并交换,此时i与j碰头,d第一次循环结束,数组a为{4,2,3,5,6},第二次循环分别把k左右两边看成一个数组,继续上述操作...
6.3代码
//快速排序
void quickSort(int*array,int left,int right){
if(right<=left)
return;
int key=array[left];
int i=left;
int j=right;
while(i<j){
for(;j>i&&array[j]>=key;j--);
array[i]=array[j];
for(;j>i&&array[i]<=key;i++);
array[j]=array[i];
}
array[i]=key;
quickSort(array,left,i-1);
quickSort(array,i+1,right);
}
6.4算法分析
快速排序(Quicksort)是对冒泡排序的一种改进
7.计数排序(Counting Sort)
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法
7.1算法原理
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
7.2举例
对于数组a{2,1,4,3,1},找出数组中最大元素max=4、最小元素min=1,重新构造一个大小为(max+1)的数组b,找出数组a中每个值出现的次数,将其存入b,则b{0,2,1,1,1},构造数组c,将b中所有计数累加至c,则c{0,2,3,4,5},构造数组result,则a中元素2排序之后的排名应该为c[2]=3,同理a中元素4的排名为a[4]=5,将其存入result数组,则result{1,1,2,3,4}
7.3代码
//找出数组中最大元素下标
int maxOfArray(int *array,int n){
int max=n-1;
for(int i=n-2;i>=0;i--){
if(array[max]<array[i])
max=i;
}
return max;
}
//计数排序
int* countingSort(int*array,int len,int num){
int*result=new int[num+1];
int*temp=new int[num+1];
int i;
for(i=0;i<len;i++)
temp[array[i]]++;
for(i=1;i<num+1;i++)
temp[i]+=temp[i-1];
for(i=len-1;i>=0;i--)
result[--temp[array[i]]]=array[i];
return result;
}
7.4算法分析
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
8)基数排序(Radix Sort)
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前
8.1算法原理
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
8.2举例
对于73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再进行一次分配,这次是根据十位数来分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。
8.3代码
//找出数组中最大元素下标
int maxOfArray(int *array,int n){
int max=n-1;
for(int i=n-2;i>=0;i--){
if(array[max]<array[i])
max=i;
}
return max;
}
//寻找数组中数字的最大位数
int findDigit(int*array,int num){
int max=maxOfArray(array,num);
int d=0;
int suffix=1;
while(array[max]/suffix>=1){
d+=1;
suffix*=10;
}
return d;
}
//基数排序
void radixSort(int*array,int num){
int digit=findDigit(array,num);
int *temp=new int[num];//盛放中间数字
int *digits=new int[10];//盛放0~9
int div=1;
//排序digit
for(int i=0;i<digit;i++){
//类似于基数排序
//清空temp数组
for(int a=0;a<10;a++)
digits[a]=0;
for(int b=0;b<num;b++){
int suffix=(array[b]/div)%10;
digits[suffix]++;
}
for(int c=1;c<10;c++){
digits[c]+=digits[c-1];
}
for(int d=num-1;d>=0;d--){
int suffix=(array[d]/div)%10;
temp[--digits[suffix]]=array[d];
}
for(int e=0;e<num;e++)
array[e]=temp[e];
div*=10;
}
}
8.4算法分析
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
3.源码
链接:https://pan.baidu.com/s/1CSD8yiI0I1NCYgjQoOxyPw
提取码:l7uj