排序算法之冒泡,选择,插入,快排

例如:待排序序列     5,4,3,7,2,7,(目的是升序排列)

 1.冒泡排序:

排n趟,每趟只是将原序列变得相对有序,随着趟数的增加,有序性也得到提升,最终完全有序

 5,4,3,7,2,7

第一趟,5>4,交换---》   4,5,3,7,2,7

               5>3 ,交换---》 4,3,5,7,2,7

               5<=7,不交换

              7>2  ,交换,---》4,3,5,2,7,7

第一趟冒泡结果:4,3,5,2,7,7 , (其中  4,5,7,7有序)

第二趟冒泡结果(重复第一次的比较方式):3,4,2,5,7,7(其中3,4,5,7,7有序

第三趟冒泡结果:3,2,4,5,7,7(其中3,4,5,7,7和2,4,  有序

第四趟冒泡结果:2,3,4,5,7,7(其中2,3,4,5,7,7有序

第五趟冒泡:没有发生交换,冒泡到此结束

规律:每趟比较都是将大的元素交换到后面

   第一趟比较,就能将最大元素交换到最后

   第二趟比较,就能将第二大元素交换到倒数第二个位置

   ******

   那么每趟比较的元素数量是呈现 --1的趋势,这和插入排序有点像

 

java代码实现:


    public int [] testBubbleSort(int []data){
    
     int maxPos=0;
     for(int i=0;i<data.length;i++)
     {
//         data.length-1-i及其之后的元素是排好序的
         for(int j=0;j<data.length-i-1;j++)
         {
             if(data[j]>data[j+1])
             {
                 //将最大位置与i交换
                 int temp =data[j];
                 data[j]=data[j+1];
                 data[j+1]=temp;
             }
         }

     }
        return data;

    }

优化:

  1. 某趟排序未发生交换,表示排序已经完成
  2. 记录上一次的最后一次交换记录,减小下一次的扫描区间:
    / 比如 初始是  1,-1,3,5,2,7,8,9
    //第一趟扫描之后:-1,1,3,2,5 ,7,8,9 最后一次2交换位置是(下标)   3
    //第二趟扫描之后: -1,1,2,3,5,7,8,9  最后一次2交换位置是2(下标)  扫描范围也应该是 [0,3] 而不是[0,到length]
    

 优化版本java代码:

/**
 * @author wangwei
 * @date 2019/1/24 18:49
 * @classDescription 冒泡排序:
 * 思想:每趟比较,将较大的向后交换,有序子序列从后往前扩张
 * 稳定性:稳定
 */
public class BubbleSort implements Sortable {
    @Override
    public void sort(int[] array) {
        //这只是一个小小的优化
        //优化1:冒泡排序结束的条件:最后一趟没有发生交换
        //优化2:第i趟发生的最后一次交换位置未被记录,导致第i+1 趟会重复 扫描未上一次没有发生交换的位置
        // 比如 初始是  1,-1,3,5,2,7,8,9
        //第一趟扫描之后:-1,1,3,2,5 ,7,8,9 最后一次2交换位置是(下标)   3
        //第二趟扫描之后: -1,1,2,3,5,7,8,9  最后一次2交换位置是2(下标)  扫描范围也应该是 [0,3] 而不是[0,到length]


        boolean exchanged = true;
        int m = array.length-1;
        while (m > 1 && exchanged) {
            exchanged = false;
            int lastSwapIndex = 1;//记录最后一次交换位置,初始化为1
            for (int j = 0; j <= m-1; j++) {
                if (array[j] > array[j + 1]) {
                    ArrayUtil.swap(array, j, j + 1);
                    exchanged = true;
                    lastSwapIndex = j;
                }
            }
            m = lastSwapIndex;
            if (!exchanged) {
                return;
            }

        }

    }
}

 

 

2.选择排序:

使用一个单位的空间来存放当前比较对象序列中的第一个元素,该趟的目标是找出这段比较序列中的最小元素及其下标,将其交换这段比较序列头部

第一趟:比较序列是  5,4,3,7,2,7

初始minPos=0;

   minVal=array[0]=5

在这一趟比较中,会出现  4<minVal=5   需要更新最小值:minVal=4,minPos=1

                                               3<minVal=4 需要更新最小值:minVal=3,minPos=2

                                                2<minVal=3 需要更新最小值:minVal=2,minPos=4

第一趟比较完之后,将第一趟找出的最小元素交换到序列头部

也就是 第一趟比较之前:   5,4,3,7,2,7,

第一趟比较之后交换结果:2,4,3,7,5,7

第二趟比较序列是:2, 4,3,7,5,7

比较交换后结果: 2,3,4,7,5,7

第三趟比较序列是:2,3,4,7,5,7

第i趟比较序列是:

array[i-1],array[i],....,array[array.length-1]

每比较完一趟,下一趟比较的元素数量比上一趟少一个

java代码实现

**
 * @author  wangwei
 * @date     2019/1/24 10:47
 * @classDescription  简单选择排序
 *   排序思想:从无序子序列中"选择"关键字最小(升序排列)或最大的记录,并将其加入到有序子序列中,以此扩充有序子序列长度
 *    排序稳定性:不稳定,由于使用了交换
 */
public class SimpleSelectSort implements Sortable{
    @Override
    public void sort(int[] array) {
     for(int lastSortedIndex=0;lastSortedIndex<array.length;lastSortedIndex++){
         int minIndex=lastSortedIndex;
         for(int lookIndex=lastSortedIndex;lookIndex<array.length;lookIndex++){
             if(array[lookIndex]<array[minIndex]){
                 minIndex=lookIndex;
             }
         }
         if(minIndex!=lastSortedIndex){
             ArrayUtil.swap(array,lastSortedIndex,minIndex);
         }
     }
    }
}

3,插入排序:

模拟摸牌的过程,手中的牌始终是有序的,需要申请空间array.length(这个也是不一定的,如果直接将原数组在逻辑上分为有序与无序的两部分,也就不需要分配array.length 的空间来单独存储有序序列)

空间复杂度:o(1)

时间复杂度:o(n^2)

稳定性:稳定

java代码实现:

/**
 * @author  wangwei
 * @date     2019/1/20 13:26
 * @classDescription  实现简单插入排序
 * 排序思想:模拟摸牌,手中牌始终是有序的,新元素插入手中,需要判断是否需要挪动
 *        空间:o(1)
 *        时间:o(n^2)
 *        稳定性:稳定
 */
public class SimpleInsertSort implements Sortable{
    @Override
    public void sort(int[] array) {
     int currIndex=1;///标记有序和无序元素的分界点 [0,currIndex) 表示有序区间
        //随着有序区间不断的扩增,直到完全覆盖整个数组,表示完成排序
        //currIndex处元素即为待排序元素
        for(;currIndex<array.length;currIndex++){
          int toBePut=array[currIndex];
          //如果是值在有序数组范围内,需要考虑挪动,放入
          if(toBePut<array[currIndex-1]){
              //leftIndex表示有序序列最大下标
              int leftIndex = currIndex-1;
              while ((leftIndex >= 0) && (array[leftIndex] > toBePut)) {
                  array[leftIndex + 1] = array[leftIndex];
                  leftIndex--;
              }
              array[leftIndex+1]=toBePut;
          }
        }
    }
    /**
     * 思考简单插入排序的改进:
     * 1,查找新元素的适合位置 时,需要o(n)次比较,可以使用二分查找,改进为lg(n)
     * 2,减少排序的规模:使用希尔排序,划分组
     */
}

生成随机数组的代码:

 public static  int []getArrayRand(int length)
    {
        int []result=new int[length];
        for(int i=0;i<result.length;i++)
        {
            result[i]=(int)(Math.random()*10000);
        }
        return result;
    }

此时速率比较:快排>插入排序>选择排序>冒泡排序

20万条元素排序时间(毫秒):

插入: 5593

选择:1,6547(冒泡的改良,避免了大量无效的交换)

冒泡:8,2772(慢如蜗牛,主要原因是大量无效的交换)

快速:47(飞一样的感觉)

4.归并排序

思想:将大规模的数组不断地二分,直到每个分区是一个元素,然后执行两个有序分区间的合并,直到合并成一个分区,完成排序

时间复杂度:o(nlgn) ,每趟归并时间复杂度是o(n) ,总共需要归并lgn 趟

空间复杂度:o(n)

 

java代码实现:

注意这里使用了两个版本

第一个是直接每次merge时,申请空间,存储归并后的有序结果,然后拷贝至原数组对应区间

第二个版本是排序开始之前,就申请了一个与待排序数组等大的数组tempArrays,每次归并操作都是将有序结果合并在tempArrays然后将数据拷贝至原数组对应区间

前者需要的空间是:o(nlgn)

后者只需要:o(n)


/**
 * @author wangwei
 * @date 2019/1/24 15:09
 * @classDescription 归并排序
 * 思想:将整体无序的原数组,分成若干规模小到可以直接排序的分组,每个分组完成排序后,两两合并(两个有序数组的合并)
 * 最终达到整体有序的状态
 */
public class MergeSort implements Sortable {
    @Override
    public void sort(int[] array) {
  int []tempArray=new int[array.length];
//      mergeSort(array,0,array.length-1);
        mergeSort2(array,tempArray,0,array.length-1);
    }

    void mergeSort(int[] array, int leftPos, int rightPos) {

        if(leftPos<rightPos){
            int center=(leftPos+rightPos)/2;
            mergeSort(array,leftPos,center);
            mergeSort(array,center+1,rightPos);
            merge(array,leftPos,center+1,rightPos);

        }



    }
    //执行合并操作

    /**
     *
     * @param array
     * @param leftPos  第一个有序区域的起始索引
     * @param rightPos  第二个有序区域的起始索引
     * @param rightEnd  第二个有序区域的结束索引
     */
    void merge(int []array,int leftPos,int rightPos,int rightEnd){
        final int leftEnd= rightPos-1;//第一个有序区域的最后的一个下标
        final int eleCount=rightEnd-leftPos+1;
        final  int start=leftPos;
//        int tempPos=leftPos;
        int []tempArrays=new int[eleCount];
        int tempPos=0;//tempArrays索引
       while (leftPos<=leftEnd && rightPos<=rightEnd){
           if(array[leftPos]<=array[rightPos]){
               tempArrays[tempPos++]=array[leftPos++];
           }else{
               tempArrays[tempPos++]=array[rightPos++];
           }
       }
       //处理两个数组合并之后仍然有一个数组剩余数据,直接放入tempArray
        while (leftPos<=leftEnd){
            tempArrays[tempPos++]=array[leftPos++];
        }
        while (rightPos<=rightEnd){
            tempArrays[tempPos++]=array[rightPos++];
        }
        //拷贝tempArray 到原数组中
        //注意只是:区间 leftPos至rightEnd区间 原数组的此区间元素已经归并放在tempArrays中
//        System.arraycopy(tempArray,0,array,0,array.length);
        RandomUtil.printArray(tempArrays);
        for(int i=0;i<eleCount;i++){
            array[start+i]=tempArrays[i];
           // System.out.println(tempArray[rightEnd]);
        }

    }
    void mergeSort2(int []array,int []tempArray,int leftPos,int rightPos){
        if(leftPos<rightPos){
            int center=(leftPos+rightPos)/2;
            mergeSort2(array,tempArray,leftPos,center);
            mergeSort2(array,tempArray,center+1,rightPos);
            merge2(array,tempArray,leftPos,center+1,rightPos);

        }
    }
    void merge2(int []array,int []tempArrays,int leftPos,int rightPos,int rightEnd){
        final int leftEnd= rightPos-1;//第一个有序区域的最后的一个下标
        final int eleCount=rightEnd-leftPos+1;
        int tempPos=leftPos;
        while (leftPos<=leftEnd && rightPos<=rightEnd){
            if(array[leftPos]<=array[rightPos]){
                tempArrays[tempPos++]=array[leftPos++];
            }else{
                tempArrays[tempPos++]=array[rightPos++];
            }
        }
        //处理两个数组合并之后仍然有一个数组剩余数据,直接放入tempArray
        while (leftPos<=leftEnd){
            tempArrays[tempPos++]=array[leftPos++];
        }
        while (rightPos<=rightEnd){
            tempArrays[tempPos++]=array[rightPos++];
        }
        //拷贝tempArray 到原数组中
        //注意只是:区间 leftPos至rightEnd区间 原数组的此区间元素已经归并放在tempArrays中
        RandomUtil.printArray(tempArrays);
        for(int i=0;i<eleCount;i++,rightEnd--){
            array[rightEnd]=tempArrays[rightEnd];
        }

    }
}

5.快速排序:

两个标记i,和j分别指向数组两端

选取一个k值,表示比较的对象,枢纽

目的是为了将比k小的元素,放到k的左侧,比k大的元素放在k的右侧

待排序数组:

 i    j   
下标012345   
元素543727   

i=0,j=5

k=array[i]=5;

part1 从右往左比较:

j=5时,array[ j ]=7>k,不交换

j--;

j=4时,array[ j ]=2<k=5,交换

目前数组:

 i   j    
下标012345   
元素243757   

k=array[ j ] =5;

part2  从左往右比较

此时:i=0;j=4

k=5>array[i]=2,不交换

i++

k=5>array[i]=array[1]=4,不交换

i++

k=5>array[i]=array[2]=3,不交换

i++

k=5<=array[i]=array[3]=7,交换

交换结果:

    ij    
下标012345   
元素243577   

现在从右往左比较,回到part1

k=array[i]=array[3]=5

k=5<array[j]=7,不交换

j--

出现i和j碰头,结束以k=5的比较,对k=5这一元素的左右两侧分别执行上述操作,part1+part2

第一次快排的结果:

    ij    
下标012345   
元素243577   

数组1:2,4,3

数组2:7,7

5,4,3,7,2,7,

java代码实现

public void quickSort(int []array,int from,int to){
        if(from<0||to>=array.length){
            return;
        }
        if(from>=to){
            return;
        }
        int kVal=array[from];
        int kPos=from;
//        int kPos=(from+to)/2;
//        int kVal=array[kPos];
        int low=from;
        int high=to;
        while(low<high){
            //从右往左找比kVal小的元素
            while(low<high && array[high]>=kVal ){
                high--;
            }
            //从右往左出现比kVal小的元素了,交换
            if(high>low ){
                array[kPos]=array[high];
               // array[high]=kVal;
                kPos=high;
                low++;
            }
            //从左往右,找比kVal大的元素
            while(low<high && array[low]<=kVal ){
                low++;
            }
            //找到比kVal大的元素,交换
            if(low<high ){
                array[kPos]=array[low];
               // array[low]=kVal;
                kPos=low;
                high--;
            }
        }
        array[kPos]=kVal;

        //到此时,kPos左边全是比kVal小的元素
        //           右边全是比kVal大的元素
        
            quickSort(array,from,kPos-1);

            quickSort(array,kPos+1,to);
     



    }

 

快排的优化在于:枢纽的选取,尽可能居中,这样就能减少递归的次数

选取枢纽的方式:

  1. 取首元素或者是尾元素(如果完全逆序或者是顺序,则会性能急剧下降,退化成冒泡排序)
  2. 随机数法(较耗时)
  3. 三者取中法(比较平衡,相比1,2)

三者取中 java代码实现:


/**
 * @author wangwei
 * @date 2019/1/24 18:58
 * @classDescription 快速排序 思想: 不断地选取枢纽("中间值"),大,小的元素分居两侧,
 * 比如: 1,4,3,5,6,2
 * 假设选取  3 为中间值
 * 第一趟之后:1,2,3,5,6,4
 * 稳定性:不稳定
 * <p>
 * <p>
 * 需要注意是:优化需要考虑 "中间值"的选取方式
 * 常用的选取方式:1,取首元素(不一定能取到靠中的位置,比如 1,2,3,4,5,6)  有序与逆序情况这是糟糕的
 * 2,随机数法 (比较耗时)
 * 3,三者取中(耗时短)
 * <p>
 * 待排序数组的特点也会影响排序效率:如果数组接近有序,快排会退化为冒泡排序
 */
public class QuickSort implements Sortable {
    @Override
    public void sort(int[] array) {
        qkSort(array, 0, array.length - 1);
//        quickSort(array,0,array.length-1);
    }

    void qkSort(int[] array, int low, int high) {

        if (low >= high) {
            return;
        }

        //获取枢纽值,并将其放在当前待处理序列末尾

        //这里的keyPos 是选取的三者取中的方法,当然也可以采用随机数,(耗时)),取首元素(对于完全有序或逆序,会很差的性能)
        dealPivot(array, low, high);
        int keyPos = high - 1;
        System.out.println(keyPos);
        int keyVal = array[keyPos];
        int left = low +1;
        int right = high-1;
        while (left < right) {
            //从左往右找到第一个比kVal大的元素 x1
            while ((left < right) && array[left] <= keyVal) {
                left++;
            }
            //从右往左找到第一个比kVal 小的元素 x2
            //从右往左,大的往右交换
            while ((left < right) && array[right] >= keyVal) {
                right--;
            }
            //交换 x1,x2
            if (left < right) {
                ArrayUtil.swap(array, left, right);

            }


        }
        //防止出现left与right碰头处元素大于kVal,如果不发生交换,此次排序相当于无用
        if (array[right] > keyVal) {
            ArrayUtil.swap(array, right, keyPos);
        }
        qkSort(array, low, right - 1);
        qkSort(array, right + 1, high);


    }

    // 三者取中  将 两端 与中间位置元素比较,将中间大小的数据放在数组倒数第二个位置,最小放在开头,最大放在末尾
    private void dealPivot(int[] array, int left, int right) {

//比如:1 4 2 4 7 3  8    首:1,中:4,尾:8  排序为  1,4,8  调整后数据变为  1,4,2,7,3,4,8
        int mid = (left + right) / 2;
        if (array[mid] > array[right]) {
            ArrayUtil.swap(array, mid, right);
        }
        if (array[left] > array[right]) {
            ArrayUtil.swap(array, left, right);
        }
        if (array[left] > array[mid]) {
            ArrayUtil.swap(array, left, mid);
        }
        ArrayUtil.swap(array, mid, right - 1);
    }

总结:

1,冒泡排序:每趟总是从左至右与相邻元素比较,一趟会将当前待排序序列中的最大的那个元素交换到最后

(注意:并不一定每次只是完成一个元素的排序,也就是该趟的最后一次交换位置,可以记住,减少下次排序规模)

2,选择排序:与冒泡类似,但是不会频繁的交换元素,使用一个空间记录当前待排序序列中的最大值,该趟扫描完,放到序列最后,待排序规模-1

3,插入排序:类比摸取扑克牌,申请一个输入规模一样大的空间(也可以不申请,只是在原数组逻辑上分为有序与无序区间),存储已排好序的元素

4,归并排序:将大数组不断二分,将各个有序子数组归并成一个有序数组,最终完成排序

5,快速排序:通过分而治之的思想排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值