图片截取自:
算法复杂度:
图片截取自:
文章参考了:
所需辅助空间最多:归并排序
所需辅助空间最少:堆排序平均速度最快:快速排序不稳定:快速排序,希尔排序,堆排序。1.冒泡排序
冒泡排序应该属于最简单的排序算法了。冒泡排序其实就是通过比较相邻位置的元素大小,如果左边比右边大,就交换位置,继续比较,实际上就是每轮比较都得出一个最大值(或者最小值)。然后通过n-1轮比较,就能得出一个排好序的序列(通过设置一个flag,当数组基本有序的时候其实不一定需要比较到n-1轮)。
具体代码:
// 1.冒泡排序 // 其实就是通过比较相邻位置的元素大小,如果左边比右边大,就交换位置,继续比较,实际上就是每轮比较都得出一个最大值(或者最小值) // 然后通过n-1轮比较,就能得出一个排好序的序列(通过设置一个flag,当数组基本有序的时候其实不一定需要比较到n-1轮) public static void bubbleSort(int[] a) { // 此处的i是用来标识现在正在进行的是第几轮比较,每轮比较都会得出一个最大值(或者最小值,此处为最大值) // 因此如果数组有n个元素的话,也就是说a.length值为n,那么我们只需要比较n-1次就能排好序,所以当 // i值为n的时候我们就直接退出循环 for (int i = 1; i < a.length; i++) { // 这里j为元素的下标,而我们在每轮比较的时候需要比较的最后一个元素的下标为n-i,也就是a.length-i for (int j = 0; j < a.length - i; j++) { // 进行两个相邻具体元素的比较,如果前者比后者大,就将二者交换,类似“冒泡”效果(其实处为“下沉”) int temp; if (a[j] > a[j + 1]) { temp = a[j]; a[j] = a[j + 1]; a[j + 1] = temp; } } } }
2.快速排序
快速排序简单来讲就是我们选定一个数,然后比它小的都放在它左边,大于等于它的都放在它右边,那么这个时候对这个数来讲他的位置已经排到了正确的地方了,接下来要做的就是在它的左右两边分别再进行类似操作。
具体代码如下:
// 2.快速排序// a为待排序的数组,l,r为数组下标(l:left,r:right)// 该函数的作用是对数组a[]从下标“l”到下标“r”这部分进行排序// 注意,当我们调用此方法的时候1应该传0,r应该传a.length - 1public static void quickSort(int[] a, int l, int r) { if (l < r) { int i = l, j = r, x = a[i]; // 跳出循环的条件是i == j; // 此时已经找到了数值x应该被插入的位置(满足其坐标你的数都比它小,右边的数都比它大) while (i < j) { // 从右往左寻找第一个小于x的那个数 while (i < j && a[j] >= x) { j--; } a[i] = a[j]; // 从左往右寻找第一个大于等于x的那个数 while (i < j && a[i] <= x) { i++; } a[j] = a[i]; } // 将数值x插入到其应该位于的位置(此时i==j,当然也可以用a[j] = x) a[i] = x; // 现在我们只是找到了一个数所属的位置,接下来我们需要在其两边分别再次进行类似寻找 // 进行递归调用 quickSort(a, i + 1, r); quickSort(a, l, i - 1); }}
3.直接插入排序
直接插入排序就是我们不断的将数插入一个已经排好序的数列中,形成新的数列,当我们从第一个(准确讲应该是从第二个数)开始不断插入,直到把所有的数都插入了进去,这是我们就得到了一个有序的数列,或者说是数组。
具体代码:
// 3.直接插入排序// 其实就是将元素一个个插入一个已经排好序了的序列中,形成新的有序序列,// 刚开始排序的时候,我们实际上认为只有一个元素的时候其本身是有序的,// 通过比较第二个元素与第一个元素的大小来决定插入其左边或者右边,以此类推...public static void insertSort(int[] a) { // 用于记录下当前需要插入的元素a[i]的值 int temp; // 要插入的元素下标为i,我们从i为1的元素开始插入,一直到将最后一个元素(下标为n-1)插入进去 for (int i = 1; i < a.length; i++) { temp = a[i]; int j ; // 如果遇到比我们要插入的元素的值大的元素,那么我们就将它向右移动1位 // (这时它自身的位置就会空出来,而这个位置就用来插入另一个比我们待插入元素值大的元素或者直接就是我们的待插入元素) for (j = i - 1; j >= 0 && a[j] > temp; j--) { a[j + 1] = a[j]; } // 将temp赋给j+1,是因为在循环中有“j--”,当不满足的时候其实下标j已经个指向了我们需要插入位置的前一个元素了 a[j + 1] = temp; }}
4.希尔排序
希尔排序又称为减小增量排序,实际上就是“分组+直接插入排序”,我们将一组数组分成一定的组,然后每组分别进行直接插入排序,这样对每个组而言得到的都是排好了序的数列,然后我们再将分的组的数量逐渐减小,这样每个组内相邻元素的间隔也会减小(组数=组距),当然,这里的组数比较通常的叫法叫“步长”,或者说是“gap”.组数会越来越小,当最后只有一组的时候,我们在经过一轮比较之后就将整个数组排好序了(注意,当组距为1的时候还没有排好)
具体代码:
// 4.希尔排序(最小增量排序),它的本质其实就是“分组+直接插入排序”// 希尔排序实际上就是不断的减小步长(gap),其实就是组距,也就是一个组内,两个相邻元素的间距,然后对每一个组进行直接插入排序// 这个gap=组距=组数// 我们不断的减小步长,当步长为1的时候,这时我们就只剩下一个组了,而这时通过比较相邻元素的大小,我们就可以得到// 最终的有序序列public static void shellSort(int[] a) { int temp; // 不断的减小步长(组距),直到它为0,当组距为零的时候我们就得到了一个有序数组 // gap=组距=组数 for (int gap = a.length / 2; gap > 0; gap /= 2) { for (int i = gap; i < a.length; i += gap) { temp = a[i]; int j = i - gap; for (; j >= 0 && a[j] > temp; j -= gap) { a[j + gap] = a[j]; } a[j + gap] = temp; } }}
5.简单选择排序
简单选择排序其实跟冒泡排序有点相似,也是每一趟比较都能得到一个最大或者最小值,只不过冒泡排序是依次进行相邻元素的比较并且交换,来实现类似生活中“冒泡”的效果,而简单选择排序的不同点就在于,虽然它也进行依次比较,但是它并不是频繁的进行交换操作,而是不断的记下已经比较过的数中的最大值的下标,比较的时候就是通过这个下标代表的值来进行比较,最后把那个得到最大值赋值给该趟比较的数组的最后后一位,如此循环n-1次(改进的话可能需要的次数更少),就可以讲一个数组排好序。
具体代码:
// 5.简单选择排序// 在要排序的一组数中,选出最小的一个数与第一个位置的数交换;// 然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。public static void selectSort(int[] a) { int minId, temp; // 只需要比较n-1趟,i表示第几趟 for (int i = 1; i < a.length; i++) { // i-1为第n趟遍历的时候的第一个元素的下标 minId = i - 1; // 将第一个元素跟后面的元素比较,并记录下最小的那个元素,实际上进行比较的就是这个最小值 for (int j = i; j < a.length; j++) { if (a[minId] > a[j]) { minId = j; } } if (minId != i-1) { temp = a[i]; a[i] = a[minId]; a[minId] = temp; } }}
6.堆排序
堆排序在这几个排序算法中应该属于比较复杂的了。因为它还涉及到二叉树,堆的知识。其实涉及到的只是也不难,就是一些概念性的东西而已,下面我们来简单的用自己的话来介绍一下,具体严严谨的定义可以自行百度或者google.
二叉树,顾名思义一个节点最多只能有两个子节点,而完全二叉树就是除了最后一层之外,每一层个都是满的,就是说除最后一层之外,剩余节点组成的数是个满二叉树。而我们这里要用到的堆,它就是一个完全二叉树。
我们从根节点开始,逐层(每层从左到右)开始编号,0,1,2,3,...这样,我们就可以将一个堆这种数据结构存放到一个数组中。如果某个节点的下标为i,那么他的左根节点下标就为2i+1,右根节点就为2i+2,而我们常用到的堆为大顶堆(和小顶堆),大顶堆的定义简单来讲就是父节点的值总比其子节点的值要大。
好啦,至此我们就粗略的说完了这些简单概念(没学过的同学可以看下《数据结构》相关知识)可能很多正在看这篇文章的同学跟我最初接触堆排序的时候的一样,有个相同的疑惑:这堆排来排去有什么用,也没见数组有序,不久得到了一个最大值嘛...对,重点就在这个最大值。听我给你细细道来...那么接下来我们就来具体看下堆排序的思想及具体实现步骤:
1.由给定的数组构建一个大顶堆,这时我们其实就已经得到了这个数组中最大值了(下标为0的那个元素,也就是堆顶元素,或者说根节点)最大值?回想一下冒泡排序和简单选择排序,有没有发现什么相同点?对的,那两个也是最大值。其实这里跟那两个算分的思想也有点相同,也是每次都获取到一个最大值。不同点嘛,看下一条..2.将那个最大值和数组的最后一个元素交换位置
此时最后一个元素就变成了最大值了3.将最后一个元素(现在已经为最大值了)之前的这些元素组成的数组重新构造一个大顶堆(因为顺序已经发生了变化)
4.如此循环,一次次取出最大值,并且重新构造大顶堆,最终也就实现了数组的排序
具体代码:
public class HeapSort { /** * heapSort(int[] arr) * 堆排序 * @param arr * 待排序数组 * */ public static void heapSort(int[] arr) { // 要想将待排序的数组构建成一个大顶堆,我们只需要从最后一个非叶子节点开始调整就行了, // 而最后一个非叶子节点的下标为:n/2 - 1 for (int i = arr.length; i > 0; i--) { // 将数组调整为大顶堆 heapAdjust(arr, 0, i); // 交换下标为0的和下标为i的元素的值,这么做的意思实际是取出大顶堆的堆顶元素,放到数组的后面, //然后将剩余的项再次构建成大顶堆,如此循环,最终将完成数组的排序 swap(arr, 0, i-1); } } /** * 构建堆的过程 * * @param arr * 需要进行排序的数组 * @param i * 需要以该下标表示的节点为堆顶元素,构建大顶堆 * @param n * 数组的长度 */ private static void heapAdjust(int[] arr, int i, int n) { //current为当前待调整的节点的下标,这里的for循环表示从最后一个非叶子节点开始调整,依次调整到根节点 for (int current = n/2-1; current >=0; i = current--) { //child用来记录当前节点的子节点中值最大的那个节点的下标(默认为左节点) int child = current*2 +1; // child != n - 1是用来判断最后一个非叶子节点是否有右节点(当只有左节点的时候那么child的值为n-1) // 所以child不等于 n-1表示当前节点有右节点,并且当右节点的值大于左节点的值的时候, // 就将下标加1,指向右节点(当前节点的子节点中值最大的那个节点的下标) if (child != n - 1 && arr[child] < arr[child + 1]) { child++; // 将序号增加1,此时child表示的是右节点的下标 } // 将当前节点的值和其子节点中值最大的节点进行比较,如果小于最大子节点中最大值,就教过二者交换 if (arr[current] < arr[child]) { swap(arr, current, child); } } } // 交换数组中两个元素的位置 public static void swap(int[] arr, int index1, int index2) { int tmp = arr[index1]; arr[index1] = arr[index2]; arr[index2] = tmp; }}