每次循环都比较先后两个元素的大小,若是前者大于后者,则将二者进行交换。这样作会将每次循环中最大的元素替换到末尾,逐渐造成有序集合。将每次循环中的最大元素逐渐由队首转移到队尾的过程形似“冒泡”过程,故所以得名。java
一个优化冒泡排序的方法就是若是在一次循环的过程当中没有发生交换,则能够当即退出当前循环,由于此时已经排好序了(也就是时间复杂度最好状况下是的由来)。node
public int[] bubbleSort(int[] array) { if (array == null || array.length < 2) { return array; } for (int i = 0; i < array.length - 1; i++) { boolean flag = false; for (int j = 0; j < array.length - 1 - i; j++) { if (array[j] > array[j + 1]) { //这里交换两个数据并无使用中间变量,而是使用异或的方式来实现 array[j] = array[j] ^ array[j + 1]; array[j + 1] = array[j] ^ array[j + 1]; array[j] = array[j] ^ array[j + 1]; flag = true; } } if (!flag) { break; } } return array; }
每次循环都会找出当前循环中最小的元素,而后和这次循环中的队首元素进行交换。git
public int[] selectSort(int[] array) { if (array == null || array.length < 2) { return array; } for (int i = 0; i < array.length; i++) { int minIndex = i; for (int j = i + 1; j < array.length; j++) { if (array[j] < array[minIndex]) { minIndex = j; } } if (minIndex > i) { array[i] = array[i] ^ array[minIndex]; array[minIndex] = array[i] ^ array[minIndex]; array[i] = array[i] ^ array[minIndex]; } } return array; }
插入排序的精髓在于每次都会在先前排好序的子集合中插入下一个待排序的元素,每次都会判断待排序元素的上一个元素是否大于待排序元素,若是大于,则将元素右移,而后判断再上一个元素与待排序元素...以此类推。直到小于等于比较元素时就是找到了该元素的插入位置。这里的等于条件放在哪里很重要,由于它是决定插入排序稳定与否的关键。算法
public int[] insertSort(int[] array) { if (array == null || array.length < 2) { return array; } for (int i = 1; i < array.length; i++) { int temp = array[i]; int j = i - 1; while (j >= 0 && array[j] > temp) { array[j + 1] = array[j]; j--; } array[j + 1] = temp; } return array; }
希尔排序能够认为是插入排序的改进版本。首先按照初始增量来将数组分红多个组,每一个组内部使用插入排序。而后缩小增量来从新分组,组内再次使用插入排序...重复以上步骤,直到增量变为1的时候,这个时候整个数组就是一个分组,进行最后一次完整的插入排序便可结束。shell
在排序开始时的增量较大,分组也会较多,可是每一个分组中的数据较少,因此插入排序会很快。随着每一轮排序的进行,增量和分组数会逐渐变小,每一个分组中的数据会逐渐变多。但由于以前已经通过了多轮的分组排序,而此时的数组会趋近于一个有序的状态,因此这个时候的排序也是很快的。而对于数据较多且趋向于无序的数据来讲,若是只是使用插入排序的话效率就并不高。因此整体来讲,希尔排序的执行效率是要比插入排序高的。数组
希尔排序执行示意图:dom
具体的实现代码以下:优化
public int[] shellSort(int[] array) { if (array == null || array.length < 2) { return array; } int gap = array.length >>> 1; while (gap > 0) { for (int i = gap; i < array.length; i++) { int temp = array[i]; int j = i - gap; while (j >= 0 && array[j] > temp) { array[j + gap] = array[j]; j = j - gap; } array[j + gap] = temp; } gap >>>= 1; } return array; }
堆排序的过程是首先构建一个大顶堆,大顶堆首先是一棵彻底二叉树,其次它保证堆中某个节点的值老是不大于其父节点的值。ui
由于大顶堆中的最大元素确定是根节点,因此每次取出根节点即为当前大顶堆中的最大元素,取出后剩下的节点再从新构建大顶堆,再取出根节点,再从新构建…重复这个过程,直到数据都被取出,最后取出的结果即为排好序的结果。spa
public class MaxHeap { /** * 排序数组 */ private int[] nodeArray; /** * 数组的真实大小 */ private int size; private int parent(int index) { return (index - 1) >>> 1; } private int leftChild(int index) { return (index << 1) + 1; } private int rightChild(int index) { return (index << 1) + 2; } private void swap(int i, int j) { nodeArray[i] = nodeArray[i] ^ nodeArray[j]; nodeArray[j] = nodeArray[i] ^ nodeArray[j]; nodeArray[i] = nodeArray[i] ^ nodeArray[j]; } private void siftUp(int index) { //若是index处节点的值大于其父节点的值,则交换两个节点值,同时将index指向其父节点,继续向上循环判断 while (index > 0 && nodeArray[index] > nodeArray[parent(index)]) { swap(index, parent(index)); index = parent(index); } } private void siftDown(int index) { //左孩子的索引比size小,意味着索引index处的节点有左孩子,证实此时index节点不是叶子节点 while (leftChild(index) < size) { //maxIndex记录的是index节点左右孩子中最大值的索引 int maxIndex = leftChild(index); //右孩子的索引小于size意味着index节点含有右孩子 if (rightChild(index) < size && nodeArray[rightChild(index)] > nodeArray[maxIndex]) { maxIndex = rightChild(index); } //若是index节点值比左右孩子值都大,则终止循环 if (nodeArray[index] >= nodeArray[maxIndex]) { break; } //不然进行交换,将index指向其交换的左孩子或右孩子,继续向下循环,直到叶子节点 swap(index, maxIndex); index = maxIndex; } } private void add(int value) { nodeArray[size] = value; size++; //构建大顶堆 siftUp(size - 1); } private void extractMax() { //将堆顶元素和最后一个元素进行交换 swap(0, size - 1); //此时并无删除元素,而只是将size-1,剩下的元素从新构建成大顶堆 size--; //从新构建大顶堆 siftDown(0); } public int[] heapSort(int[] array) { if (array == null || array.length < 2) { return array; } nodeArray = new int[array.length]; for (int value : array) { add(value); } for (int i = 0; i < array.length; i++) { extractMax(); } return nodeArray; } }
上面的经典实现中,若是须要变更节点时,都会来一次父子节点的互相交换操做(包括删除节点时首先作的要删除节点和最后一个节点之间的交换操做也是如此)。若是仔细思考的话,就会发现这实际上是多余的。在须要交换节点的时候,只须要siftUp操做时的父节点或siftDown时的孩子节点从新移到当前须要比较的节点位置上,而比较节点是不须要移动到它们的位置上的。此时直接进入到下一次的判断中,重复siftUp或siftDown过程,直到最后找到了比较节点的插入位置后,才会将其插入进去。这样作的好处是能够省去一半的节点赋值的操做,提升了执行的效率。同时这也就意味着,须要将要比较的节点做为参数保存起来,而在ScheduledThreadPoolExecutor源码中也正是这么实现的(《较真儿学源码系列-ScheduledThreadPoolExecutor(逐行源码带你分析做者思路)》)。
归并排序使用的是分治的思想,首先将数组不断拆分,直到最后拆分红两个元素的子数组,将这两个元素进行排序合并,再向上递归。不断重复这个拆分和合并的递归过程,最后获得的就是排好序的结果。
合并的过程是将两个指针指向两个子数组的首位元素,两个元素进行比较,较小的插入到一个temp数组中,同时将该数组的指针右移一位,继续比较该数组的第二个元素和另外一个元素…重复这个过程。这样temp数组保存的即是这两个子数组排好序的结果。最后将temp数组复制回原数组的位置处便可。
public int[] mergeSort(int[] array) { if (array == null || array.length < 2) { return array; } return mergeSort(array, 0, array.length - 1); } private int[] mergeSort(int[] array, int left, int right) { if (left < right) { //这里没有选择“(left + right) / 2”的方式,是为了防止数据溢出 int mid = left + ((right - left) >>> 1); // 拆分子数组 mergeSort(array, left, mid); mergeSort(array, mid + 1, right); // 对子数组进行合并 merge(array, left, mid, right); } return array; } private void merge(int[] array, int left, int mid, int right) { int[] temp = new int[right - left + 1]; // p1和p2为须要对比的两个数组的指针,k为存放temp数组的指针 int p1 = left, p2 = mid + 1, k = 0; while (p1 <= mid && p2 <= right) { if (array[p1] <= array[p2]) { temp[k++] = array[p1++]; } else { temp[k++] = array[p2++]; } } // 把剩余的数组直接放到temp数组中 while (p1 <= mid) { temp[k++] = array[p1++]; } while (p2 <= right) { temp[k++] = array[p2++]; } // 复制回原数组 for (int i = 0; i < temp.length; i++) { array[i + left] = temp[i]; } }
快速排序的核心是要有一个基准数据temp,通常取数组的第一个位置元素。而后须要有两个指针left和right,分别指向数组的第一个和最后一个元素。
首先从right开始,比较right位置元素和基准数据。若是大于等于,则将right指针左移,比较下一位元素;若是小于,就将right指针处数据赋给left指针处(此时left指针处数据已保存进temp中),left指针+1,以后开始比较left指针处数据。
拿left位置元素和基准数据进行比较。若是小于等于,则将left指针右移,比较下一位元素;而若是大于就将left指针处数据赋给right指针处,right指针-1,以后开始比较right指针处数据…重复这个过程。
直到left和right指针相等时,说明这一次比较过程完成。此时将先前存放进temp中的基准数据赋值给当前left和right指针共同指向的位置处,便可完成这一次排序操做。
以后递归排序基础数据的左半部分和右半部分,递归的过程和上面讲述的过程是同样的,只不过数组范围再也不是原来的所有数组了,而是如今的左半部分或右半部分。当所有的递归过程结束后,最终结果即为排好序的结果。
快速排序执行示意图:
正如上面所说的,通常取第一个元素做为基准数据,但若是当前数据为从大到小排列好的数据,而如今要按从小到大的顺序排列,则数据分摊不均匀,时间复杂度会退化为,而不是正常状况下的
。此时采起一个优化手段,即取最左边、最右边和最中间的三个元素的中间值做为基准数据,以此来避免时间复杂度为
的状况出现,固然也能够选择更多的锚点或者随机选择的方式来进行选取。
还有一个优化的方法是:像快速排序、归并排序这样的复杂排序方法在数据量大的状况下是比选择排序、冒泡排序和插入排序的效率要高的,可是在数据量小的状况下反而要更慢。因此咱们能够选定一个阈值,这里选择为47(和源码中使用的同样)。当须要排序的数据量小于47时走插入排序,大于47则走快速排序。
private static final int THRESHOLD = 47; public int[] quickSort(int[] array) { if (array == null || array.length < 2) { return array; } return quickSort(array, 0, array.length - 1); } private int[] quickSort(int[] array, int start, int end) { // 若是当前须要排序的数据量小于等于THRESHOLD则走插入排序的逻辑,不然继续走快速排序 if (end - start <= THRESHOLD - 1) { return insertSort(array); } // left和right指针分别指向array的第一个和最后一个元素 int left = start, right = end; /* 取最左边、最右边和最中间的三个元素的中间值做为基准数据,以此来尽可能避免每次都取第一个值做为基准数据、 时间复杂度可能退化为O(n^2)的状况出现 */ int middleOf3Indexs = middleOf3Indexs(array, start, end); if (middleOf3Indexs != start) { swap(array, middleOf3Indexs, start); } // temp存放的是array中须要比较的基准数据 int temp = array[start]; while (left < right) { // 首先从right指针开始比较,若是right指针位置处数据大于temp,则将right指针左移 while (left < right && array[right] >= temp) { right--; } // 若是找到一个right指针位置处数据小于temp,则将right指针处数据赋给left指针处 if (left < right) { array[left] = array[right]; left++; } // 而后从left指针开始比较,若是left指针位置处数据小于temp,则将left指针右移 while (left < right && array[left] <= temp) { left++; } // 若是找到一个left指针位置处数据大于temp,则将left指针处数据赋给right指针处 if (left < right) { array[right] = array[left]; right--; } } // 当left和right指针相等时,此时循环跳出,将以前存放的基准数据赋给当前两个指针共同指向的数据处 array[left] = temp; // 一次替换后,递归交换基准数据左边的数据 if (start < left - 1) { array = quickSort(array, start, left - 1); } // 以后递归交换基准数据右边的数据 if (right + 1 < end) { array = quickSort(array, right + 1, end); } return array; } private int middleOf3Indexs(int[] array, int start, int end) { int mid = start + ((end - start) >>> 1); if (array[start] < array[mid]) { if (array[mid] < array[end]) { return mid; } else { return array[start] < array[end] ? end : start; } } else { if (array[mid] > array[end]) { return mid; } else { return array[start] < array[end] ? start : end; } } } private void swap(int[] array, int i, int j) { array[i] = array[i] ^ array[j]; array[j] = array[i] ^ array[j]; array[i] = array[i] ^ array[j]; }
以上的七种排序算法都是比较排序,也就是基于元素之间的比较来进行排序的。而下面将要介绍的三种排序算法是非比较排序,首先是计数排序。
计数排序会建立一个临时的数组,里面存放每一个数出现的次数。好比一个待排序的数组是[3, 3, 5, 2, 7, 4, 2],那么这个临时数组中记录的数据就是[2, 2, 1, 1, 0, 1]。表示2出现了两次、3出现了两次、4出现了一次、5出现了一次、6出现了零次、7出现了一次。那么最后只须要遍历这个临时数组中的计数值就能够了。
public int[] countingSort(int[] array) { if (array == null || array.length < 2) { return array; } //记录待排序数组中的最大值 int max = array[0]; //记录待排序数组中的最小值 int min = array[0]; for (int i : array) { if (i > max) { max = i; } if (i < min) { min = i; } } int[] temp = new int[max - min + 1]; //记录每一个数出现的次数 for (int i : array) { temp[i - min]++; } int index = 0; for (int i = 0; i < temp.length; i++) { //当输出一个数以后,当前位置的计数就减一,直到减到0为止 while (temp[i]-- > 0) { array[index++] = i + min; } } return array; }
从上面的实现中能够看到,计数排序仅适合数据跨度不大的场景。若是最大值和最小值之间的差距比较大,生成的临时数组就会比较长。好比说一个数组是[2, 1, 3, 1000],最小值是1,最大值是1000。那么就会生成一个长度为1000的临时数组,可是其中绝大部分的空间都是没有用的,因此这就会致使空间复杂度变得很高。
计数排序是稳定的排序算法,但在上面的实现中并无体现出这一点,上面的实现没有维护相同元素之间的前后顺序。因此须要作些变换:将临时数组中从第二个元素开始,每一个元素都加上前一个元素的值。仍是拿以前的[3, 3, 5, 2, 7, 4, 2]数组来举例。计完数后的临时数组为[2, 2, 1, 1, 0, 1],此时作上面的变换,每一个数都累加前面的一个数,结果为[2, 4, 5, 6, 6, 7]。这个时候临时数组的含义就再也不是每一个数出现的次数了,此时记录的是每一个数在最后排好序的数组中应该要存放的位置+1(若是有重复的就记录最后一个)。对于上面的待排序数组来讲,最后排好序的数组应该为[2, 2, 3, 3, 4, 5, 7]。也就是说,此时各个数最后一次出现的索引位为:1, 3, 4, 5, 6,分别都+1后就是2, 4, 5, 6, 7,这不就是上面作过变换以后的数组吗?(没有出现过的数字无论它)因此,此时从后往前遍历原数组中的每个值,将其减去最小值后,找到其在变换后的临时数组中的索引,也就是找到了最后排好序的数组中的位置了。固然,每次找到临时数组中的索引后,这个位置的数须要-1。这样若是后续有重复的该数字的话,就会插入到当前位置的前一个位置了。由此也说明了遍历必须是从后往前遍历,以此来维护相同数字之间的前后顺序。
public int[] stableCountingSort(int[] array) { if (array == null || array.length < 2) { return array; } //记录待排序数组中的最大值 int max = array[0]; //记录待排序数组中的最小值 int min = array[0]; for (int i : array) { if (i > max) { max = i; } if (i < min) { min = i; } } int[] temp = new int[max - min + 1]; //记录每一个数出现的次数 for (int i : array) { temp[i - min]++; } //将temp数组进行转换,记录每一个数在最后排好序的数组中应该要存放的位置+1(若是有重复的就记录最后一个) for (int j = 1; j < temp.length; j++) { temp[j] += temp[j - 1]; } int[] sortedArray = new int[array.length]; //这里必须是从后往前遍历,以此来保证稳定性 for (int i = array.length - 1; i >= 0; i--) { sortedArray[temp[array[i] - min] - 1] = array[i]; temp[array[i] - min]--; } return sortedArray; }
上面的计数排序在数组最大值和最小值之间的差值是多少,就会生成一个多大的临时数组,也就是生成了一个这么多的桶,而每一个桶中就只插入一个数据。若是差值比较大的话,会比较浪费空间。那么我能不能在一个桶中插入多个数据呢?固然能够,而这就是桶排序的思路。桶排序相似于哈希表,经过必定的映射规则将数组中的元素映射到不一样的桶中,每一个桶内进行内部排序,最后将每一个桶按顺序输出就好了。桶排序执行的高效与否和是不是稳定的取决于哈希散列的算法以及内部排序的结果。须要注意的是,这个映射算法并非常规的映射算法,要求是每一个桶中的全部数都要比前一个桶中的全部数都要大。这样最后输出的才是一个排好序的结果。好比说第一个桶中存1-30的数字,第二个桶中存31-60的数字,第三个桶中存61-90的数字...以此类推。下面给出一种实现:
public int[] bucketSort(int[] array) { if (array == null || array.length < 2) { return array; } //记录待排序数组中的最大值 int max = array[0]; //记录待排序数组中的最小值 int min = array[0]; for (int i : array) { if (i > max) { max = i; } if (i < min) { min = i; } } //计算桶的数量(能够自定义实现) int bucketNumber = (max - min) / array.length + 1; List<Integer>[] buckets = new ArrayList[bucketNumber]; //计算每一个桶存数的范围(能够自定义实现或者不用实现) int bucketRange = (max - min + 1) / bucketNumber; for (int value : array) { //计算应该放到哪一个桶中(能够自定义实现) int bucketIndex = (value - min) / (bucketRange + 1); //延迟初始化 if (buckets[bucketIndex] == null) { buckets[bucketIndex] = new ArrayList<>(); } //放入指定的桶 buckets[bucketIndex].add(value); } int index = 0; for (List<Integer> bucket : buckets) { //对每一个桶进行内部排序,我这里使用的是快速排序,也可使用别的排序算法,固然也能够继续递归去作桶排序 quickSort(bucket); if (bucket == null) { continue; } //将不为null的桶中的数据按顺序写回到array数组中 for (Integer integer : bucket) { array[index++] = integer; } } return array; }
基数排序不是根据一个数的总体来进行排序的,而是将数的每一位上的数字进行排序。好比说第一轮排序,我拿到待排序数组中全部数个位上的数字来进行排序;第二轮排序我拿到待排序数组中全部数十位上的数字来进行排序;第三轮排序我拿到待排序数组中全部数百位上的数字来进行排序...以此类推。每一轮的排序都会累加上一轮全部前几位上排序的结果,最终的结果就会是一个有序的数列。
基数排序通常是对全部非负整数进行排序的,可是也能够有别的手段来去掉这种限制(好比都加一个固定的数或者都乘一个固定的数,排完序后再恢复等等)。基数排序和桶排序很像,桶排序是按数值的区间进行划分,而基数排序是按数的位数进行划分。同时这两个排序都是须要依靠其余排序算法来实现的(若是不算递归调用桶排序自己的话)。基数排序每一轮的内部排序会使用到计数排序来实现,由于每一位上的数字无非就是0-9,是一个小范围的数,因此使用计数排序很合适。
基数排序执行示意图:
具体的实现代码以下:
public int[] radixSort(int[] array) { if (array == null || array.length < 2) { return array; } //记录待排序数组中的最大值 int max = array[0]; for (int i : array) { if (i > max) { max = i; } } //获取最大值的位数 int maxDigits = 0; while (max != 0) { max /= 10; maxDigits++; } //用来计数排序的临时数组 int[] temp = new int[10]; //用来存放每轮排序后的结果 int[] sortedArray = new int[array.length]; for (int d = 1; d <= maxDigits; d++) { //每次循环开始前都要清空temp数组中的值 replaceArray(temp, null); //记录每一个数出现的次数 for (int a : array) { temp[getNumberFromDigit(a, d)]++; } //将temp数组进行转换,记录每一个数在最后排好序的数组中应该要存放的位置+1(若是有重复的就记录最后一个) for (int j = 1; j < temp.length; j++) { temp[j] += temp[j - 1]; } //这里必须是从后往前遍历,以此来保证稳定性 for (int i = array.length - 1; i >= 0; i--) { int index = getNumberFromDigit(array[i], d); sortedArray[temp[index] - 1] = array[i]; temp[index]--; } //一轮计数排序事后,将此次排好序的结果赋值给原数组 replaceArray(array, sortedArray); } return array; } private final static int[] sizeTable = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000}; /** * 获取指定位数上的数字是多少 */ private int getNumberFromDigit(int number, int digit) { if (digit < 0) { return -1; } return (number / sizeTable[digit - 1]) % 10; } private void replaceArray(int[] originalArray, int[] replaceArray) { if (replaceArray == null) { for (int i = 0; i < originalArray.length; i++) { originalArray[i] = 0; } } else { for (int i = 0; i < originalArray.length; i++) { originalArray[i] = replaceArray[i]; } } }
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|---|
平均状况 | 最好状况 | 最坏状况 | |||
冒泡排序 | ![]() |
![]() |
![]() |
![]() |
稳定 |
选择排序 | ![]() |
![]() |
![]() |
![]() |
不稳定 |
插入排序 | ![]() |
![]() |
![]() |
![]() |
稳定 |
希尔排序 | 取决于增量的选择 | ![]() |
不稳定 | ||
堆排序 | ![]() |
![]() |
![]() |
![]() |
不稳定 |
归并排序 | ![]() |
![]() |
![]() |
![]() |
稳定 |
快速排序 | ![]() |
![]() |
![]() |
![]() |
不稳定 |
计数排序 | ![]() |
![]() |
![]() |
![]() |
稳定 |
桶排序 | 取决于桶散列的结果和内部排序算法的时间复杂度 | ![]() |
稳定 | ||
基数排序 | ![]() |
![]() |
![]() |
![]() |
稳定 |
(其中:
在几十年的计算机科学发展中,诞生了不少优秀的算法,你们都在为了能开发出更高效的算法而努力。可是在这其中也诞生了一些仅供娱乐的搞笑算法,猴子排序就是其中的一种。
猴子排序的实现很简单,随机找出两个元素进行交换,直到随机交换到最后能正确排好序的时候才会中止。
public int[] bogoSort(int[] array) { if (array == null || array.length < 2) { return array; } Random random = new Random(); while (!inOrder(array)) { for (int i = 0; i < array.length; i++) { int swapPosition = random.nextInt(i + 1); if (swapPosition == i) { continue; } array[i] = array[i] ^ array[swapPosition]; array[swapPosition] = array[i] ^ array[swapPosition]; array[i] = array[i] ^ array[swapPosition]; } } return array; } private boolean inOrder(int[] array) { for (int i = 0; i < array.length - 1; i++) { if (array[i] > array[i + 1]) { return false; } } return true; }
原创不易,未得准许,请勿转载,翻版必究