七大排序帮你忙

270 阅读11分钟

冒泡排序

代码实现

public void sort(int arr[]){
    int temp = 0;
    for(int i = 0; i < arr.length; i++){
        for(int j = arr.length - 1; j > i; j--){
            if(arr[j] > arr[j - 1]){
                temp = arr[j];
                arr[j] = arr[j - 1];
                arr[j - 1] = temp;
            }
        }
    }

}

原理分析

这个原理想必大家都烂熟于心,但我还是叨叨几句。冒泡排序,顾名思义就是从数组末尾,两两比较,将最大或最小值冒泡到数组头部。

选择排序

代码实现

public void easySort(int arr[]){
    for(int i = 0; i < arr.length; i++){
        int min = i;
        int temp;
        for( int j = i + 1; j < arr.length; j++){
            if(arr[min] > arr[j]){
                min = j;
            }
        }
        temp = arr[i];
        arr[i] = arr[min];
        arr[min] = temp;
    }
}

原理分析

在未排序的序列中找出最小(大)元素与第一个位置的元素交换位置。

选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。

插入排序

代码实现

public void insertSort(int arr[]){
    int temp, i;
    for(i = 1; i < arr.length; i++){
        temp = arr[i];
        int j = i;
        while (j > 0 && temp < arr[j - 1]){
            arr[j] = arr[j - 1];    //移位法
            j--;
        }
        arr[j] = temp;
    }
}

原理分析

工作原理为构建有序序列,对于未排序元素,在已排序序列中从后向前扫描,找到相应位置并插入。

  • 从第一个元素开始,该元素可认为已排序。
  • 取出下一个元素,在排序好的元素序列中从后往前扫描
  • 如果元素(已排序)大于新元素,将该元素移到下一位置
  • 重复3.直到找到已排序的元素小于或等于新元素的位置
  • 将新元素插入该位置后
  • 重复2-5直到排序完成

希尔排序

代码实现

public void shellSort(int arr[]){
    int temp = 0;
    for(int gap = arr.length /2; gap > 0; gap /= 2){
        // 遍历各组中所有的元素(共gap组), 步长gap
        for(int i = gap; i < arr.length; i++){
            for(int j = i - gap; j >= 0; j -= gap){
                if(arr[j] > arr[j + gap]){
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
}

原理分析

先将整个待排序的记录序列分割成为若干子序列分别进行俩俩交换,待整个序列中的记录“基本有序”时,再对全体记录进行依次冒泡排序。

首先,选择增量 gap ,初始增量为 gap = 10/2 = 5,整个数组分成了 5 组
按颜色划分为【 8 , 3 】,【 9 , 5 】,【 1 , 4 】,【 7 , 6 】,【 2 , 0 】
对这分开的 5 组分别使用俩俩比较

结果可以发现,这五组中的相对小元素都被调到前面了

缩小增量 gap = 5/2 = 2,整个数组分成了 2 组
【 3 , 1 , 0 , 9 , 7】,【 5 , 6 , 8 , 4 , 2】
对这分开的 2 组分别使用俩俩比较

此时整个数组的有序性是很明显的

再缩小增量 gap = 2/2 = 1,整个数组分成了 1 组
【 0, 2 , 1 , 4 , 3 , 5 , 7 , 6 , 9 , 0】

此时,只需要对以上数列进行简单的微调,不需要大量的移动操作即可完成整个数组的排序

快速排序

代码实现

public void sort(int arr[], int low, int high){
    if(low < high){
        // 找寻基准数据的正确索引
        int index = getIndex(arr,low,high);
        // 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
        sort(arr,0,index - 1);
        sort(arr,index + 1,high);
    }
}
public int getIndex(int arr[],int low, int high){
        // 基准数据
        int temp = arr[low];
        while (low < high){
            // 当队尾的元素大于等于temp时,向前挪动high指针
            while (low < high && arr[high] > temp){
                high--;
            }
            // 如果队尾元素小于temp了,需要将其赋值给low
            arr[low] = arr[high];
            
            // 当队首元素小于等于temp时,向前挪动low指针
            while (low < high && arr[low] < temp){
               low++;
            }
            // 当队首元素大于temp时,需要将其赋值给high
            arr[high] = arr[low];
        }
        // 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
        // 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
        arr[high] = temp;
         // 返回tmp的正确位置
        return high;
    }

原理分析

  • 先从队尾开始向前扫描且当low < high时,如果a[high] > tmp,则high–,但如果a[high] < temp,则将high的值赋值给low,即arr[low] = arr[high],同时要转换数组扫描的方式,即需要从队首开始向队尾进行扫描了。
  • 同理,当从队首开始向队尾进行扫描时,如果arr[low] < tmp,则low++,但如果arr[low] > temp了,则就需要将low位置的值赋值给high位置,即arr[low] = arr[high],同时将数组扫描方式换为由队尾向队首进行扫描。
  • 不断重复①和②,知道low>=high时(其实是low=high),low或high的位置就是该基准数据在数组中的正确索引位置。

归并排序

代码实现

//分+合方法
public void merget(int arr[], int left, int right, int temp[]){
    if(right > left){
        int mid = (left + right) / 2;
        merget(arr, left, mid, temp);
        merget(arr, mid + 1, right, temp);
        mergetSort(arr, left, mid, right, temp);
    }

}
/**
 * 合并的方法
 * @param arr 排序的原始数组
 * @param left 左边有序序列的初始索引
 * @param mid 中间索引
 * @param right 右边索引
 * @param temp 做中转的数组
 */
public void mergetSort(int arr[], int left, int mid, int right, int temp[]){
    int i = left, j = mid + 1, t = 0;
    //(一)
    //先把左右两边(有序)的数据按照规则填充到temp数组
    //直到左右两边的有序序列,有一边处理完毕为止
    while(i <= mid && j <= right){
        //如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
        //即将左边的当前元素,填充到 temp数组 
        //然后 t++, i++
        temp[t++] = arr[i] > arr[j] ? arr[j++] : arr[i++];
    }
    
    //(二)
    //把有剩余数据的一边的数据依次全部填充到temp
    while(i <= mid){
        temp[t++] = arr[i++];
    }
    while(j <= right){
        temp[t++] = arr[j++];
    }
    
    //(三)
    //将temp数组的元素拷贝到arr
    t = 0;
    while (left <= right){
        arr[left++] = temp[t++];
    }
}

原理分析

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。

它将数组平均分成两部分: center = (left + right)/2,当数组分得足够小时---数组中只有一个元素时,只有一个元素的数组自然而然地就可以视为是有序的,此时就可以进行合并操作了。因此,上面讲的合并两个有序的子数组,是从 只有一个元素 的两个子数组开始合并的。

合并后的元素个数:从 1-->2-->4-->8......

比如初始数组:[24,13,26,1,2,27,38,15]

  • 分成了两个大小相等的子数组:[24,13,26,1] [2,27,38,15]
  • 再划分成了四个大小相等的子数组:[24,13] [26,1] [2,27] [38,15]
  • 此时,left < right 还是成立,再分:[24] [13] [26] [1] [2] [27] [38] [15]

此时,有8个小数组,每个数组都可以视为有序的数组了!!!,每个数组中的left == right,从递归中返回(从19行--20行的代码中返回),故开始执行合并():

mergetSort([24],[13]) 得到 [13,24]

mergetSort([26],[1]) 得到[1,26]

堆排序

代码实现

public void heapSort(int arr[]){
    //将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
    for(int i = arr.length / 2 - 1; i >= 0; i--){
        adjustHeap(arr, i, arr.length);
    }
    /**
     * 2).将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;	
     * 3).重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
     */
    int temp;
    for(int j = arr.length - 1; j > 0; j--){
        temp = arr[0];
        arr[0] = arr[j];
        arr[j] = temp;
        adjustHeap(arr, 0 ,j);
    }
}
/**
 * 功能: 完成 将 以 i 对应的非叶子结点的树调整成大顶堆
 * 举例  int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adjustHeap => 得到 {4, 9, 8, 5, 6}
 * 如果我们再次调用  adjustHeap 传入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {9,6,8,5, 4}
 * @param arr 待调整的数组
 * @param i 表示非叶子结点在数组中索引
 * @param length 表示对多少个元素继续调整, length 是在逐渐的减少
 */
public void adjustHeap(int arr[], int i, int length){
    //先取出当前元素的值,保存在临时变量
    int temp = arr[i];
    //k = i * 2 + 1 k 是 i结点的左子结点
    for(int k = i * 2 + 1; k < length; k = k * 2 + 1){
        //说明左子结点的值小于右子结点的值
        //k + 1 < length 是为了防止越界
        if(k + 1 < length && arr[k] < arr[k + 1]){
            // k 指向右子结点
            k++;
        }
        //如果子结点大于父结点
        if(temp < arr[k]){
            //把较大的值赋给当前结点
            arr[i] = arr[k];
            //i 指向 k,继续循环比较
            i = k;
        }else {
            break;
        }
    }
    //当for 循环结束后,我们已经将以i 为父结点的树的最大值,放在了 最顶(局部)
    arr[i] = temp;
}

原理分析

先说明什么是大顶堆、小顶堆:
大顶堆:每个结点的值都大于或等于其左右孩子结点的值小顶堆;
小顶堆:每个结点的值都小于或等于其左右孩子结点的值。

堆排序总共分三步:

  • 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
  • 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端
  • 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

总结而言:每次交换都是在一个非叶子节点以及其左右子节点的二叉树中进行的。基于这个理念,不断堆非叶子节点进行遍历,最终形成大顶堆或小顶堆。每次都将当前的最大值放入数组末尾,然后减少参与的元素个数。

基数排序(桶排序)

代码实现

public void radix(int arr[]){
    int max = arr[0];
    for(int i = 1; i < arr.length; i++){
        if(arr[i] > max)
            max = arr[i];
    }
    int maxlen = (max + "").length();
    //定义一个二维数组,表示10个桶, 每个桶就是一个一维数组
    //说明
    //1. 二维数组包含10个一维数组
    //2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
    //3. 名明确,基数排序是使用空间换时间的经典算法
    int bucket[][] = new int[10][arr.length];

    //为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
    //可以这里理解
    //比如:bucketElementCounts[0] , 记录的就是  bucket[0] 桶的放入数据个数
    int bucketnum[] = new int[10];
    for(int i = 0, n = 1; i < maxlen; i++, n *= 10){
        //(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位..
        for(int j = 0; j < arr.length; j++){
            int num = arr[j] / n % 10;
            bucket[num][bucketnum[num]] = arr[j];
            bucketnum[num]++;
        }
        //按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
        int index = 0;
        for(int j = 0; j < bucketnum.length; j++){
            //如果桶中,有数据,我们才放入到原数组
            if(bucketnum[j] != 0){
                //循环该桶即第k个桶(即第k个一维数组), 放入
                for(int k = 0; k < bucketnum[j]; k++){
                    //取出元素放入到arr
                    arr[index++] = bucket[j][k];
                }
            }
            //第j轮处理后,需要将每个 bucketnum[j] = 0 !!!!
            bucketnum[j] = 0;
        }
    }
}

原理分析

桶排序的原理很有趣,并不是像上面几种需要绕弯子。

  • 根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数;
  • 遍历待排序集合,将每一个元素移动到对应的桶中;
  • 对每一个桶中元素进行排序,并移动到已排序集合中。

步骤疑问:

  • 首先如何确定桶的个数,一般来说默认为10。
  • 其次如何确定对应桶,举个例子 【2,1,55,99,845,231】
    • 我们先按个位数排序,那么2放在2号桶,1放在1号桶,依次求出。
    • 然后依次取出,组成新顺序的数组。
    • 之后在执行第一步,取十位数进行放置(接下来可取百位数,千位数......直至最大位数)

经过上述的操作,数组最终将有序。

本文gif来自排序算法-动图演示