前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java实现八种排序算法详解

Java实现八种排序算法详解

作者头像
对话、
发布2022-02-22 14:39:11
3120
发布2022-02-22 14:39:11
举报
文章被收录于专栏:Android-Xj

插入排序

基本思想:在要排序的一组数中,假设前面(n-1)[n>=2]个数已经是排好顺序的,现在要把第n个数插到前面的有序数中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。

插入排序
插入排序

总体分析:

  • 用第二个数和第一个数比较大小,大的放到右边。
  • 用第三个数分别和第二个数还有第一个数比较,并把大的放到右边。
  • 用第四个数分别和第一个第三个第二个第一个数比较,并把大的放到右边。

所以这里肯定要用到嵌套for循环。

代码语言:javascript
复制
    public void insertSort(int[] arr) {
	int len = arr.length;
	//要插入的数
	int insertNum;
	//因为第一次不用,所以从1开始
	for (int i = 1; i < len; i++) {
		insertNum = arr[i];
		//序列元素个数
		int j = i - 1;
		//从后往前循环,将大于insertNum的数向后移动
		while (j >=0 && arr[j] > insertNum) {
			//元素向后移动
			arr[j + 1] = arr[j];
			j--;
		}
		//找到位置,插入当前元素
		arr[j + 1] = insertNum;
	}
}

希尔排序

针对直接插入排序的下效率问题,有人对次进行了改进与升级,这就是现在的希尔排序。希尔排序,也称递减增量排序算法, 是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。 希尔排序是1959年由D.L.Shell提出来的,相对直接插入排序有较大的改进。希尔排序的实质就是分组插入排序,该方法又称缩小增量排序。

基本思想:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序, 然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。 因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的. 因此希尔排序在时间效率上比前两种方法有较大提高。步长的选择是希尔排序的重要部分。 只要最终步长为1任何步长序列都可以工作。

希尔排序
希尔排序

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
  • 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位

对于直接插入排序问题,数据量巨大时。 将数的个数设为n,取奇数k=n/2,将下标差值为k的数分为一组,构成有序序列。 再取k=k/2 ,将下标差值为k的书分为一组,构成有序序列。 重复第二步,直到k=1执行简单插入排序。

算法最开始以一定的步长进行排序,然后会继续以一定步长进行排序,最终算法以步长为1进行排序。 当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。 Donald Shell最初建议步长选择为\frac{n}{2}并且对步长取半直到步长达到 1。 虽然这样取可以比\mathcal{O}(n^2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。

代码语言:javascript
复制
希尔排序示例:n=10的一个数组 58 27 32 93 65 87 58 46 9 65,步长为n/2。

第一次排序 步长为 10/2 = 5

   58  27  32  93  65  87  58  46  9  65 
   1A                  1B
       2A                  2B
           3A                  3B
                4A                 4B
                    5A                5B


首先将待排序元素序列分组,以5为步长,(1A,1B),(2A,2B),(3A,3B)等为分组标记,大写字母表示是该组的第几个元素,
数字相同的表示在同一组,这样就分成5组,即(58,87),(27,58),(32,46),(93,9),(65,65),
然后分别对各分组进行直接插入排序,排序后5组为(58,87),(27,58),(32,46),(9,93),(65,65),
分组排序只是变得各个分组内的下表,下同。

第二次排序 步长为 5/2 = 2

58  27  32  9  65  87  58  46  93  65

1A      1B      1C      1D      1E

    2A      2B      2C      2D        2E

第三次排序 步长为 2/2 = 1

32  9  58  27  58  46  65  65  93  87

1A  1B  1C  1D  1E  1F  1G  1H  1I  1J

第四次排序 步长为 1/2 = 0 得到有序元素序列

9  27  32  46  58  58  65  65  87  93
代码语言:javascript
复制
    public void shellSort(int[] arr) {
    int len = arr.length;
    while (len != 0) {
        len = len / 2;
        //分组
        for (int i = 0; i < len; i++) {
            //元素从第二个开始
            for (int j = i + len; j < arr.length; j += len) {
                //k为有序序列最后一位的位数
                int k = j - len;
                //要插入的元素
                int temp = arr[j];
                //从后往前遍历
                while (k >= 0 && temp < arr[k]) {
                    arr[k + len] = arr[k];
                    //向后移动len位
                    k -= len;
                }
                arr[k + len] = temp;
            }
        }
    }
}

简单选择排序

基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换; 然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。

选择排序
选择排序

思路:

  • 第一次用第一个数字去和后面的每个数字比较,将小的放到第一个位置,这样完成这一轮之后,第一个数就是最小的数。
  • 第二次用第二个数字去和后面的每个数字比较,将晓得放到第二个位置,这样完成这一轮之后,第二个数就是最第二小的数。
代码语言:javascript
复制
public void selectSort(int[] arr) {
        int len = arr.length;
        for (int i = 0; i < len; i++) {
            int value = arr[i];
            int position = i;
            //找到最小的值和位置
            for (int j = i + 1; j < len; j++) {
                if (arr[j] < value) {
                    value = arr[j];
                    position = j;
                }
            }
            //进行交换
            arr[position] = arr[i];
            arr[i] = value;
        }
    }

堆排序

对简单选择排序的优化。 基本思想:堆排序是一种树形选择排序,是对直接选择排序的有效改进。

(heap),这里所说的堆是数据结构中的堆,而不是内存模型中的堆。堆数据结构是一种特殊的二叉树,在这棵树中,所有父节点都满足大于等于其子节点的堆叫大根堆,所有父节点都满足小于等于其子节点的堆叫小根堆。堆虽然是一颗树,但是通常存放在一个数组中,父节点和孩子节点的父子关系通过数组下标来确定。如下图的小根堆及存储它的数组:

堆排序
堆排序

堆的介绍:

  • 堆是完全二叉树
  • 常常用数组实现
  • 每一个节点的关键字都大于(等于)这个节点的子节点的关键字

由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。 初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。 然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。 从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。

步骤:

  • 将序列构建成大顶堆。
  • 将根节点与最后一个节点交换,然后断开最后一个节点。
  • 重复第一、二步,直到所有节点断开。
代码语言:javascript
复制
public void heapSort(int[] arr) {
        int len = arr.length;
        //循环建堆
        for (int i = 0; i < len - 1; i++) {
            //建堆
            buildMaxHeap(arr, len - 1 - i);
            //交换堆顶和最后一个元素
            swap(arr, 0, len - 1 - i);
        }
    }

    //交换方法
    private void swap(int[] data, int i, int j) {
        int tmp = data[i];
        data[i] = data[j];
        data[j] = tmp;
    }

    //对data数组从0到lastIndex建大顶堆
    private void buildMaxHeap(int[] data, int lastIndex) {
        //从lastIndex处节点(最后一个节点)的父节点开始
        for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
            //k保存正在判断的节点
            int k = i;
            //如果当前k节点的子节点存在
            while (k * 2 + 1 <= lastIndex) {
                //k节点的左子节点的索引
                int biggerIndex = 2 * k + 1;
                //如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在
                if (biggerIndex < lastIndex) {
                    //若果右子节点的值较大
                    if (data[biggerIndex] < data[biggerIndex + 1]) {
                        //biggerIndex总是记录较大子节点的索引
                        biggerIndex++;
                    }
                }
                //如果k节点的值小于其较大的子节点的值
                if (data[k] < data[biggerIndex]) {
                    //交换他们
                    swap(data, k, biggerIndex);
                    //将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值
                    k = biggerIndex;
                } else {
                    break;
                }
            }
        }
    }

冒泡排序

步骤:

  • 将序列中所有元素两两比较,将最大的放在最后面。
  • 将剩余序列中所有元素两两比较,将最大的放在最后面。
  • 重复第二步,直到只剩下一个数。
冒泡排序
冒泡排序
代码语言:javascript
复制
public void bubbleSort(int[] arr) {
        int len = arr.length;
        for (int i = 0; i < len; i++) {
            for (int j = 0; j < len - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }

快速排序

基本思想:找出一个元素(理论上可以随便找一个)作为基准(pivot),然后对数组进行分区操作, 使基准左边元素的值都不大于基准值,基准右边的元素值 都不小于基准值,如此作为基准的元素调整到排序后的正确位置。 递归快速排序,将其他n-1个元素也调整到排序后的正确位置。最后每个元素都是在排序后的正 确位置,排序完成。 所以快速排序算法的核心算法是分区操作,即如何调整基准的位置以及调整返回基准的最终位置以便分治递归。

选择基准元

  1. 固定基准元 如果输入序列是随机的,处理时间是可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。因为每次划分只能使待排序序列减一,此时为最坏情况,快速排序沦为冒泡排序,时间复杂度为Θ(n^2)。而且,输入的数据是有序或部分有序的情况是相当常见的。因此,使用第一个元素作为基准元是非常糟糕的,应该立即放弃这种想法。
  2. 随机基准元 这是一种相对安全的策略。由于基准元的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n2)。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2n)。所以随机化快速排序可以对于绝大多数输入数据达到O(n×log(n))的期望时间复杂度。
  3. 三数取中 最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为基准元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准元。
快速排序
快速排序

partition算法

partition算法是快速排序的核心,在学习快排之前,可以先学习一下这个算法。下面先贴代码:

代码语言:javascript
复制
public int partition(int[] num, int left, int right) {
    if (num == null || num.length <= 0 || left < 0 || right >= num.length) {
        return 0;
    }
    //获取数组中间元素的下标
    int prio = num[left + (right - left) / 2];
    //从两端交替向中间扫描
    while (left <= right) {
        while (num[left] < prio)
            left++;
        while (num[right] > prio)
            right--;
        if (left <= right) {
            //最终将基准数归位
            swap(num, left, right);
            left++;
            right--;
        }
    }
    return left;
}

这个方法的思路是先找一个枢纽元(这个方法实现里面找的是第一个元素,具体其实大有文章不过这里先简化描述), 再从数组的两边(具体从哪里到哪里由传进来额参数决定)生成两个指针leftright, 每次发现左边的元素大于枢纽元则i停下来,右边的元素小于枢纽元j就停下来,并且交换这个两个数的位置。 直到两个指针leftright相遇。再把枢纽元插入left的位置,也就是它应该在的位置。

这么做最后的结果是让数组的[left,right]部分呈现出2部分,枢纽元最终位置以左都是小于等于枢纽元的,以右都是大于等于枢纽元的。而枢纽元则被插入到了一个绝对正确的位置。

代码语言:javascript
复制
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    int[] data = new int[]{26, 53, 67, 48, 57, 13, 48, 32, 60, 50};
    quickSort(data,0,data.length-1);
}

void quickSort(int arr[], int left, int right) {
    if (left < right) {
        //算出枢轴值
        int index = partition(arr, left, right);
        //对低子表递归排序
        quickSort(arr, left, index - 1);
        //对高子表递归排序
        quickSort(arr, index + 1, right);
    }

    for (int i = 0; i < arr.length; i++) {
        Log.e("@@@", "" + arr[i]);
    }
}

public int partition(int[] num, int left, int right) {
    if (num == null || num.length <= 0 || left < 0 || right >= num.length) {
        return 0;
    }
    int prio = num[left + (right - left) / 2];   //获取数组中间元素的下标
    while (left <= right) {                 //从两端交替向中间扫描
        while (num[left] < prio)
            left++;
        while (num[right] > prio)
            right--;
        if (left <= right) {
            swap(num, left, right);        //最终将基准数归位
            left++;
            right--;
        }
    }
    return left;
}


public void swap(int[] num, int left, int right) {
    int temp = num[left];
    num[left] = num[right];
    num[right] = temp;
}

归并排序

速度仅次于快速排序,内存少的时候使用,可以进行并行计算的时候使用。 归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列, 每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

归并排序
归并排序
代码语言:javascript
复制
{
    int[] data = new int[]{26, 53, 67, 48, 57, 13, 48, 32, 60, 50};
    data = mergeSort(data, 0, data.length - 1);
    for (int i = 0; i < data.length; i++) {
        Log.e("@@@", "" + data[i]);
    }
}

public static int[] mergeSort(int[] a, int low, int high) {
    int mid = (low + high) / 2;
    if (low < high) {
        mergeSort(a, low, mid);
        mergeSort(a, mid + 1, high);
        //左右归并
        merge(a, low, mid, high);
    }
    return a;
}

public static void merge(int[] a, int low, int mid, int high) {
    int[] temp = new int[high - low + 1];
    int i = low;
    int j = mid + 1;
    int k = 0;
    // 把较小的数先移到新数组中
    while (i <= mid && j <= high) {
        if (a[i] < a[j]) {
            temp[k++] = a[i++];
        } else {
            temp[k++] = a[j++];
        }
    }
    // 把左边剩余的数移入数组
    while (i <= mid) {
        temp[k++] = a[i++];
    }
    // 把右边边剩余的数移入数组
    while (j <= high) {
        temp[k++] = a[j++];
    }
    // 把新数组中的数覆盖nums数组
    for (int x = 0; x < temp.length; x++) {
        a[x + low] = temp[x];
    }
}

基数排序

基数排序(radix sort)又称桶排序(bucket sort),相对于常见的比较排序,基数排序是一种分配式排序, 即通过将所有数字分配到应在的位置最后再覆盖到原数组完成排序的过程

用于大量数,很长的数进行排序时。

基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始, 依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

我们回想一下我们小时候是怎么学习比较数字大小的?我们是先比位数,如果一个位数比另一个位数多, 那这个数肯定更大。如果位数同样多,就按位数递减依次往下进行比较,哪个数在这一位上更大那就停止比较, 得出这个在这个位上数更大的数字整体更大的结论。当然我们也可以从最小的位开始比较, 这其实就对应了基数排序里的MSD(most significant digital)和LSD(least significant digital)两种排序方式。

想清楚了这一点之后,我们就要考虑如何存储每一位排序结果的问题了,首先既然作为分配式排序,联想计数排序, 每一位排序时存储该次排序结果的数据结构应该至少是一个长度为10的数组(对应十进制该位0-9的数字)。 同时可能存在以下情况:原数组中所有元素在该位上的数字都相同,那一维数组就没法满足我们的需要了, 我们需要一个10*n(n为数组长度)的二维数组来存储每次位排序结果。

熟悉计数排序结果的读者可能会好奇:为什么不能像计数排序一样,在每个位置只存储出现该数字的次数, 而不存储具体的值,这样不就可以用一维数组了?这个我们不妨先思考一下,在对基数排序分析完之后再来看这个问题。 现在我们可以存储每次位排序的结果了,为了在下一位排序前用到这一位排序的结果, 我们要将桶里排序的结果还原到原数组中去,然后继续对更改后的原数组执行前一步的位排序操作,如此循环, 最后的结果就是数组内元素先按最高位排序,最高位相同的依次按下一位排序,依次递推。得到排序的结果数组。

初始化:构造一个10*n的二维数组,一个长度为n的数组用于存储每次位排序时每个桶子里有多少个元素。 循环操作:从低位开始(我们采用LSD的方式),将所有元素对应该位的数字存到相应的桶子里去(对应二维数组的那一列)。 然后将所有桶子里的元素按照桶子标号从小到大取出,对于同一个桶子里的元素,先放进去的先取出, 后放进去的后取出(保证排序稳定性)。这样原数组就按该位排序完毕了,继续下一位操作,直到最高位排序完成。

基数排序
基数排序
代码语言:javascript
复制
private static void radixSort(int[] array,int d) {
    int n=1;//代表位数对应的数:1,10,100...
    int k=0;//保存每一位排序后的结果用于下一位的排序输入
    int length=array.length;
    int[][] bucket=new int[10][length];//排序桶用于保存每次排序后的结果,这一位上排序结果相同的数字放在同一个桶里
    int[] order=new int[length];//用于保存每个桶里有多少个数字
    while(n<d) {
        for(int num:array) //将数组array里的每个数字放在相应的桶里 {
            int digit=(num/n)%10;
            bucket[digit][order[digit]]=num;
            order[digit]++;
        }
        //将前一个循环生成的桶里的数据覆盖到原数组中用于保存这一位的排序结果
        for(int i=0;i<length;i++) {
            //这个桶里有数据,从上到下遍历这个桶并将数据保存到原数组中
            if(order[i]!=0) {
                for(int j=0;j<order[i];j++) {
                    array[k]=bucket[i][j];
                    k++;
                }
            }
            order[i]=0;//将桶里计数器置0,用于下一次位排序
        }
        n*=10;
        k=0;//将k置0,用于下一轮保存位排序结果
    }
}
public static void main(String[] args) {
    int[] A=new int[]{73,22, 93, 43, 55, 14, 28, 65, 39, 81};
    radixSort(A, 100);
    for(int num:A) {
        System.out.println(num);
    }
}

总结:

  • 插入排序:一般插入排序,比较是从有序序列的最后一个元素开始,如果比它大则直接插入在其后面,否则一直往前比。如果找到一个和插入元素相等的,那么就插入到这个相等元素的后面。插入排序是稳定的。
  • 希尔排序:希尔排序是按照不同步长对元素进行插入排序,一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,稳定性就会被破坏,所以希尔排序不稳定。
  • 简单选择排序:在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。光说可能有点模糊,来看个小实例:858410,第一遍扫描,第1个元素8会和4交换,那么原序列中2个8的相对前后顺序和原序列不一致了,所以选择排序不稳定。 -堆排序:堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, …这些父节点选择元素时,有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,所以堆排序并不稳定。
  • 堆排序:从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
  • 冒泡排序:由前面的内容可知,冒泡排序是相邻的两个元素比较,交换也发生在这两个元素之间,如果两个元素相等,不用交换。所以冒泡排序稳定。
  • 快速排序:在中枢元素和序列中一个元素交换的时候,很有可能把前面的元素的稳定性打乱。还是看一个小实例:6 4 4 5 4 7 8 9,第一趟排序,中枢元素6和第三个4交换就会把元素4的原序列破坏,所以快速排序不稳定。
  • 归并排序:在分解的子列中,有1个或2个元素时,1个元素不会交换,2个元素如果大小相等也不会交换。在序列合并的过程中,如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,所以,归并排序也是稳定的。
  • 基数排序:是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 插入排序
  • 希尔排序
  • 简单选择排序
  • 堆排序
  • 冒泡排序
  • 快速排序
    • 选择基准元
      • partition算法
      • 归并排序
      • 基数排序
      相关产品与服务
      GPU 云服务器
      GPU 云服务器(Cloud GPU Service,GPU)是提供 GPU 算力的弹性计算服务,具有超强的并行计算能力,作为 IaaS 层的尖兵利器,服务于生成式AI,自动驾驶,深度学习训练、科学计算、图形图像处理、视频编解码等场景。腾讯云随时提供触手可得的算力,有效缓解您的计算压力,提升业务效率与竞争力。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档