算法描述
名称 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
奇偶排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n^3/2) | O(n^2) | O(1) | 不稳定 |
快速排序 | O(nlogn) | O(n^2) | O(logn) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(n) | 稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
记数排序 | O(n+k) | O(n+k) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n+k) | 稳定 |
桶排序 | O(n) | O(n) | O(m) | 稳定 |
以下描述排序的默认为使用整型数组,从左到右按大到小排序
若排序的是非整型则比较的那步按自定义规则比较,交换整个元素即可,比如对一堆人排序按年龄大到小,则比较时用的人的年龄进行比较,交换位置时交换整个人对象。
概念:稳定性为当元素相同的前后顺序是否改变,改变则不稳定,否则稳定。
快速排序和归并排序用到的递归和转化为非递归的做法详情见《递归想法和实现》
排序的复杂度转至《大O记法解释》
动态图转自
https://www.cnblogs.com/onepixel/articles/7674659.html
-
冒泡排序
冒泡排序,最简单的排序算法,看名字冒泡比喻像水泡一样从水下往上冒,为什么水泡会往上冒呢?因为水泡相对于上面时更轻的往上浮(冒)。冒泡排序即从左到右遍历,比较两个相邻元素,如果后面元素大于前面的元素则交换位置否则不交换,然后向后一步继续比较交换执行n-1次比较,下一趟遍历因为最后一个元素已经为最小元素所以执行n-2次比较,下一趟执行n-3次……直到第一个元素即排序完毕
-
选择排序
选择排序为冒泡排序的改进版,将冒泡排序的交换次数从O(n^2)变成O(n),但比较次数仍然为O(n2),平均复杂度均一样,在交换很费劲的情况,比如需要交换的数据比较大时交换时间明显高于比较时间时执行效率会比冒泡排序优化很多(因为Java交换的是对象所以这种情况不存在)。过程为遍历整个数组选择出最大的元素然后将该元素与第一个元素交换,再从第二个位置开始遍历整个数组选择出最大的元素然后将该元素与第二个元素交换,再从第三个位置遍历。。。。左边排序好了,遍历完所有的数据即排序完成。(选择过程,用一个变量标记当前最大元素,扫描下一个元素时将当前最大元素与其比较若大于则保存当前元素作为当前最大元素继续遍历)
-
插入排序
插入排序虽然时间复杂度和冒泡选择一样,但是同常情况下会比他们快,在局部有序或者基本有序的情况这个算法就比较好。插入排序的思想就是将待排序的元素逐个插入到已经排序好的部分的适合的位置上,直到插入所有元素即排序完毕,(编码思路:取出待排序的元素放一个变量里,和已经排序好的部分尾部元素比较,若大于则尾部元素后移一位腾出位置,继续将待排序元素与挨着刚刚比较元素的元素进行比较,若还大于则将该元素也向后移动到刚刚腾出的位置然后它原来的位置腾出来,若小于则满足前大后小直接将待排序元素放在刚刚腾出的位置即可……一直大于则一直比较到第一个位置……插入完所有的待排序元素即排序结束)
-
奇偶排序
奇偶排序也是一种简单的排序,因为每个取出来的以对对是独立的,可以分别用不同的处理器比较和交换,这样可以很快的排序。奇偶排序即对排序数组进行两趟遍历,第一趟遍历奇数对即a[j]和啊[j+1]这个奇数对(其中j为奇数1.3.5.7……)(数据对为啊[1]a[2]、a[3][4]、……没有交集所以每对独立),比较如果前元素小于后面元素则交换位置否则不动。第二趟遍历偶数对a[i]a[i+1]其中i为偶数0.2.4……同样的比较如果前元素小于后面元素则交换位置否则不动,再次遍历两趟直到数组有序(有序的标志为遍历一趟下来没有交换)。这个算法能够排序成功是因为当奇数对交换后奇数对所有对都满足了,但是如果偶数对交换了位置则会影响奇数的顺序打乱之前的满足,奇数对交换也同样影响偶数对之前的状态,只有仅当奇数对和偶数对都满足时排序完成,因为奇数对和偶数对时交错的。
-
希尔排序
希尔排序是插入排序的改进版,由希尔发明的所以叫希尔排序,在插入排序中当一个比较大的元素在最左边的话需要将将所有比它小的元素都移动一位,移动量贼多,如果能不用一个一个的移动中间项就能把最大移到最左边的话算法的效率将会大大提高。基于这个思路希尔排序被提出。希尔排序增加了插入排序元素间的间隔,并在元素间进行插入排序,因为插入排序元素之间有间隔所以达到间隔插入效果,然后再减少间隔同样的操作,直到间隔为1时进行一趟插入排序因为基于前面的排序待排序数组属于基本有序所以效率大大提高。间隔序列的选取影响效率的关键虽然希尔本人提出方法时间隔序列用的是n/2,就是说有n个元素第一趟间隔n/2,第二趟n/2/2,……,但是科学家们大量的实验证明用knuth序列做为间隔效率更为理想,即满足公式h=h*3+1(1,4,13,121,346,1039……),比如10个数据则先取间隔为4后再为1,1000个数据则取346后递减为121……直到1。编程思路,先从第一个元素把所有和它间隔为规定值的元素作为一个组进行插入排序,再从第二个元素把所有和它间隔为规定值的元素作为一个组进行插入排序…直到规定间隔区间,再减少间隔距离做同样的操作直到间隔为1时进行插入排序即排序完成。
希尔排序因为一组一组进行插入排序时每一组都是独立的,所以在并行和多处理器中可以更高效排序
-
快速排序
快速排序算法是最流行的排序算法,因为有充分的理由相信大多数情况下是最快的排序算法,基本思想是取一个元素作为中枢,操作让数组里比该值大的都放到该值的左边,比该值小的都放在该值的右边,这样中枢右边的元素一定比左边的小,所以可以对左右两边进一步进行划分分别排序互不相干,然后对左边的部分和右边的部分再进行同样的操作,大的在左小的在右,然后再更小的划分……直到划分只有一个元素时排序完毕。其中划分成两小段的长度差影响到性能比较大中枢元素的选取影响到两边的长度,所以可以适当优化中枢的选取,一般优化操作是取最左和最右和中间三个元素取中值作为中枢。还有当数据小的时候小划分再划分效率没有用插入排序效率高所以另一个优化方法是当划分得比较小后换用插入排序对每个小段进行进一步排序knuth推荐当小划分为长度小于10时使用其他算法进行排序还有优化递归的做法,查看《递归想法和实现介绍》递归用栈实现的部分(优化)。。。。。。编程思路,取出中枢值(假设取最右边的值)该值的位置即为一个空位记录该位置为right,遍历从左边第一个开始往右遍历数组直到遇到比中枢值小的元素记录该位置为left,将该值赋值给right那个位置,此时该位置为空位,接着从right位置向前遍历直到遇到值大于中枢值时此时right位置变为该值的位置,再将该值赋值给left的那个空位,则right所指的位置为新的位置,接着从left开始向右……直到right和left相逢,最后把中枢值赋值给空位则划分一趟完毕(空位先在左边再到右边再左边……)。简言之left向前移动直到遇到小于中枢值得值将其丢到right处,再将right从右边往前移动直到遇到比中枢大得元素将其丢到left处再left向前移动直到遇到小于中枢值得值将其丢到right处……。
-
归并排序
归并排序基于递归的排序算法和快速排序一样通过递归划分更小处理的,速度仅次于快速排序,为稳定排序算法,比较容易实现一般用于对总体无序,但是各子项相对有序的数列。不过所需的空间比较多,需要一个等于被排序序列的空间,在内存足够时也是一个好的选择。归并排序是基于两个有序的序列归并到一个新的有序数列过程的,例题的归并过程为,从左边开始遍历一个数组和另外一个数组的最左边元素对比如果大于则将遍历到的元素放到第三个数组从左到右放继续遍历下一个,如果小于则停止遍历取出另外一个数组大于的元素放在第三个数组从左到右放继续比较当前数组的下一个元素和另外一个数组最左边元素……最后一个数组为空时另外的数组直接放入第三个数组,直到遍历完两个数组即合并结束,归并算法是基于这种合并实现的,将大数组分成两半,再分成两半再分……直到只有一个元素,一个元素是有序的,然后开始递归,一个元素合并成两个有序的元素,再有两个含有两个有序元素组合成四个有序元素组,再由两个有四个有序元素的组合并……当递归完成即排序完成,编程思路先编写一个两个有序数组合并的函数(简单的循环比较和赋值即可),然后一直分半调用自己,直到只有一个元素则开始递归即可。
-
堆排序
堆排序是一种基于堆这种数据结构的排序算法,简单而高效而且空间效率高,堆这种数据结构的特点是沿着根到每条子路劲到底保证有序的,建立堆的过程基本由无序的数据项组成而移出堆的往往是极值,将待排序的数列建立一个堆数据结构,再将其逐个顺序移出,移出来的顺序即为排序好的数列。堆这种数据结构详情见《堆这种数据结构》
-
计数排序
计数排序也是一种快速的排序算法,当元素区间不大时非常高效,计数排序需要额外的空间,当元素的值跨度越大,需要的数组空间越大,而且对于非整型的数列排序支持不友好。是一种不需要比较的排序算法,排序耗时间时也是一个好的选择。妙在妙用数组的下标提高效率。思路为先从待排序的数组中选取出最大值和最小值,建立一个长度为最大值减去最小值那么大的数组,数组中初始值全为0,然后遍历待排序数组,统计个数,每个元素减去刚刚找出的最小即为新建数组的下标,每遍历一个元素,算出数组下标将数组中的值+1,记录该数出现的次数,遍历完待排序数组后,将新建辅组的数组遍历,下标加刚刚找出的最小值即为原始值,该下标的数组元素的值即为该值的数量(0表示数量为0),将其从最大下标或者最小下标取出逐个取回原来的数组即排序完成。
-
基数排序
这也是是一个和前面完全不一样的排序可以说是和计数排序有一个共通的思想,都是统计分类思想,提供一种比较高效有趣的思路,前面都是基于比较再进一步操作的排序,基数排序则不需要比较和交换,基于分类来的。改进了计数排序所用额外空间不定带来的性能问题。这里以10为基底的运算做讲解例子,其实可以以2为基底进行运算也很高效,因为可以运用高速的位运算(先看过程体会再思考)。先创建一个长度为10的数组,和十个链表,十个链表放数组里面,找出待排序数组最大值取其位数,遍历所有待排序元素取出元素的个位数作为数组下标插入对应链表中,然后遍历辅组数组和每个数组链表中每个元素依次放回原来的数组中(注意链表的插入和取出顺序关系到下一步排序的顺利,应该使用插入顺序和取出顺序一样的列表),此时放回待排序数组中已经按个位数进行排序好了,然后再遍历所有待排序元素取出元素的十位数作为数组下标插入对应链表中(注意链表的插入和取出顺序关系到下一步排序的顺利,应该使用插入顺序和取出顺序相同的列表),然后遍历辅组数组和每个数组链表中每个元素依次放回原来的数组中,此时放回待排序数组中已经按十位数进行排序好了,由于之前一趟的排序,在十位数相同时个位数是排序好的(因为按顺序插入链表和按顺序取出链表的),然后再百位……达到最大数值的位数即排序完毕。
-
桶排序
基本思路是设计有限数量的桶(数组),将待排序的元素按一定的映射关系放入对应的桶中,对每个不为空的桶分别用其他排序算法进行排序,然后按顺序取出放回原来的数组即排序完成,也可以递归再建立桶(参考基数排序)。计数排序是通过将相同元素记录在一个数组里,而桶排序则将不同元素可以放一个桶里再进一步排序,节约了空间,牺牲了时间。桶的数量最大时就是计数排序。
性能分析,假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是O(n+mn/mlog(n/m))=O(n+nlogn-nlogm) ,从上式看出,当m接近n的时候,桶排序复杂度接近O(n) ,当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。所以桶排序适用于均匀分布的情况
代码实现
以下代码均是对数组testArray进行从左到有由大到小进行排序
testArray={45,23,32,81,53,34,78,23,34,89,89,89,89,6,23,23,637,89,91,29,575,64,45,23,525,245,3,846,80}
import java.util.LinkedList;
import java.util.List;
public class Sort {
private int[] arr;
public static void main(String[] arg){
int[] arr={45,23,32,81,53,34,78,23,34,89,89,89,89,6,23,23,637,89,91,29,575,64,45,23,525,245,3,846,80};
Sort sort=new Sort(arr);
sort.display();
sort.radixSort();
sort.display();
}
public Sort(int[] arr){
this.arr=arr;
}
//冒泡排序
public void bubbleSort(){
int length=arr.length;
int temp;
for(int i=0;i<length;i++){
for(int j=length-1;j>i;j--){
if(arr[j]>arr[j-1]){
temp=arr[j-1];
arr[j-1]=arr[j];
arr[j]=temp;
}
}
display();
}
}
//选择排序
public void selectionSort(){
int length=arr.length;
int temp;
for (int i=0;i<length;i++) {
int maxIndex=length-1;
//找出最大
for (int j=length-2;j>=i;j--){
if(arr[j]>arr[maxIndex]){
maxIndex=j;
}
}
temp=arr[i];
arr[i]=arr[maxIndex];
arr[maxIndex]=temp;
display();
}
}
//插入排序
public void insertionSort(){
int length=arr.length;
int temp;
for(int i=0;i<length-1;i++) {
temp=arr[i+1];
int j=i;
//移动
while (j>=0&&arr[j]<temp){
arr[j+1]=arr[j];
j--;
}
arr[j+1]=temp;
display();
}
}
//奇偶排序
public void oddevenSort(){
boolean ifend=false;//记录是否改动过
int temp;
while(!ifend){
for (int i=0;i<arr.length-1;i=i+2){
if(arr[i]<arr[i+1]){
temp=arr[i];
arr[i]=arr[i+1];
arr[i+1]=temp;
ifend=true;
}
}
for (int i=1;i<arr.length-1;i=i+2){
if(arr[i]<arr[i+1]){
temp=arr[i];
arr[i]=arr[i+1];
arr[i+1]=temp;
ifend=true;
}
}
if(ifend){
ifend=false;
}
else{
ifend=true;
}
display();
}
}
//希尔排序
public void shellSort(){
int length=arr.length;
int temp;
int k;
int h=1;//表示间隔,这里使用knuth序列
while(h<length){
h=3*h+1;
}
//按knuth序列递减间隔
for(h=(h-1)/3;h>0;h=(h-1)/3){
//遍历所有子列
for (int i=h;i<2*h&&i<length;i++){
//对每小子列做插入排序,注意是从第二个元素插入的和上面的有些不同
for (int j=i;j<length;j=j+h){
temp=arr[j];
k=j;
//注意这个与条件的左右顺序,否则报错
while(k>=h&&arr[k-h]<temp){
arr[k]=arr[k-h];
k=k-h;
}
arr[k]=temp;
}
}
display();
}
}
//快速排序
// 这里直接选取最右边元素作为中枢值,暂时不优化
public void quickSort(int left1,int right1) {
//这里也可以判断区间小的话改用其他排序提高效率
int left = left1;
int right = right1;
if (left1 >= right1) {
return;
}
else{
//中枢值,这里可以进行优化选择,然后将选择的中枢值和最右边的值交换一样可以用下面的代码
int p = arr[right];
//当相等时退出
while (left < right) {
//相等时也不动,防止连续几个相等时混乱
while (left < right && arr[left] >= p) {
left++;
}
//这里移动后指向下一步,防止重复比较
arr[right] = arr[left];
right--;
while (left < right && arr[right] <= p) {
right--;
}
arr[left] = arr[right];
left++;
}
//最后一遍可能多做了一次自增
if(left==right)
arr[left]=p;
else
//如果不相等一定是left多自增一次,因为它在循环的最后
arr[--left]=p;
display();
}
//中枢值位置不动
quickSort(left1, left-1);
//因为上面循环right可能多一次自增也可能不多,所以直接用left
quickSort(right+1, right1);
}
//归并排序
public void mergeSort(){
int[] workSpace=new int[arr.length];
recMergeSort(workSpace,0,arr.length-1);
}
//递归进行合并
private void recMergeSort(int[] workSpace,int left,int right){
if (right<=left)
return;
int mid=(right+left)/2;
recMergeSort(workSpace,left,mid);
recMergeSort(workSpace,mid+1,right);
display();
merge(workSpace,left,mid,right);
}
//两部分有序数组合并成以大部分有序数组
private void merge(int[] workSpace,int left,int mid,int right){
//这里左边部分取到mid,右边从mid+1开始
int j=0,left1=left,right1=mid+1;
//某一边遍历完退出
while(left1<=mid&&right1<=right){
workSpace[j++]=(arr[left1]>arr[right1])?arr[left1++]:arr[right1++];
}
//当一边为空,另一边直接放
while(left1<=mid){
workSpace[j++]=arr[left1++];
}
while(right1<=right){
workSpace[j++]=arr[right1++];
}
//取回排序好的部分
for(int i=0;i<j;i++){
arr[left++]=workSpace[i];
}
}
//堆排序,用堆实现优先队列,从队中逐个移除放回数组,直接在原来的数组实现堆排序得看堆数据结构那里
public void heapSort(){
//这里直接用集合框架PriorityQueue底层基于堆实现的优先队列
Queue<Integer> priorityQueue=new PriorityQueue<>();
for (int e:arr) {
priorityQueue.add(e);
}
for(int i=0;i<arr.length;i++){
arr[i]=priorityQueue.remove();
}
}
//计数排序
public void countingSort(){
//找出最小和最大值
int max=arr[0],min=arr[0];
for (int element:arr) {
if (element>max)
max=element;
if (element<min)
min=element;
}
int[] bucket=new int[max-min+1];
int i,j;
//由大到小排序输出
int k=arr.length-1;
//计数个数
for (i=0;i<arr.length;i++){
bucket[arr[i]-min]++;
}
//输出回原来数组
for (i=0;i<bucket.length;i++){
j=bucket[i];
while(j>0) {
arr[k--] = i + min;
j--;
}
}
}
//基数排序,可以带参也可以不带参
public void radixSort(){
//找最大值
int max=arr[0],maxRadix=0;
for (int element:arr) {
if (element>max)
max=element;
}
while(max!=0) {
max = max / 10;
maxRadix++;
}
radixSort(maxRadix);
}
public void radixSort(int maxRadix){
//创建一个存着链表的数组
LinkedList<Integer>[] bucket=new LinkedList[10];
for (int i=0;i<10;i++){
bucket[i]=new LinkedList<>();
}
int k,j,index;
for (int i=0;i<maxRadix;i++) {
k=(int)Math.pow(10,i);
//将元素放到对应的数组的链表中
for (int element : arr) {
if (i!=0){
index=element/k;
}
else{
index=element;
}
bucket[index%10].addLast(element);
}
//按顺序取出链表数组,链表用队列实现
j=0;
for (LinkedList element:bucket) {
while (!element.isEmpty()){
arr[j++]=(int)element.removeFirst();
}
}
display();
}
}
//实际上基数排序和基数排序都是桶排序的例子,这里不展示
public void bucket(){}
public void display(){
for (int a:arr) {
System.out.print(a+",");
}
System.out.println();
}
}