1. 冒泡排序
从数组头开始,比较相邻的元素。如果第一个比第二个大(小),就交换它们两个;对每一对相邻元素作同样的工作,从开始第一对到尾部的最后一对,这样在最后的元素应该会是最大(小)的数;重复步骤1~2,重复次数等于数组的长度,直到排序完成。
public static void sort(int[] array) {
if (array.length == 0)
return;
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length-i-1; j++) {
if (array[j]>array[j+1]) {
int tem = array[j];
array[j] = array[j+1];
array[j+1] = tem;
}
}
}
}
2. 简单选择排序
首先,找到数组中最大(小)的那个元素;
其次,将它和数组的第一个元素交换位置(如果第一个元素就是最大(小)元素那么它就和自己交换);
再次,在剩下的元素中找到最大(小)的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。
这种方法叫做选择排序,因为它在不断地选择剩余元素之中的最大(小)者。
for (int i = 0; i < array.length; i++) {
int min = i;
for (int j = i; j < array.length; j++) {
if (array[min]>array[j]) {
min=j;
}
}
if (min!=i) {
int tem = array[min];
array[min] = array[i];
array[i] = tem;
}
}
3. 简单插入排序
对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。为了给要插入的元素腾出空间,我们需要将插入位置之后的已排序元素在都向后移动一位。插入排序所需的时间取决于输入中元素的初始顺序。
public static void sort(int[] array) {
if (array.length == 0)
return;
for (int i = 0; i < array.length-1; i++) {
int pre = i;
int curData = array[i+1];
while(pre>=0&&array[pre]>curData){
array[pre+1]=array[pre];
pre--;
}
array[pre+1]=curData;
}
}
4. 希尔排序
希尔排序是把待排序数组按一定数量的分组,对每组使用直接插入排序算法排序;然后缩小数量继续分组排序,随着数量逐渐减少,每组包含的元素越来越多,当数量减至 1 时,整个数组恰被分成一组,排序便完成了。这个不断缩小的数量,就构成了一个增量序列。
public static void sort(int[] array) {
if (array.length == 0)
return;
int curData;
int len = array.length;
int gap=len / 2;
while(gap>0){
for (int i = 0; i <=array.length-1-gap; i++) {
int pre = i;
curData = array[i+gap];
while(pre>=0&&array[pre]>curData){
array[pre+gap]=array[pre];
pre = pre-gap;
}
array[pre+gap]=curData;
}
gap= gap/2;
}
}
5. 归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法,对于给定的一组数据,利用递归与分治技术将数据序列划分成为越来越小的半子表,在对半子表排序后,再用递归方法将排好序的半子表合并成为越来越大的有序序列。为了提升性能,有时我们在半子表的个数小于某个数(比如15)的情况下,对半子表的排序采用其他排序算法,比如插入排序。若将两个有序表合并成一个有序表,称为2-路归并,与之对应的还有多路归并。
public static int[] sort(int[] array) {
if (array.length < 2) return array;
/*切分数组,然后递归调用*/
int mid = array.length / 2;
int[] left = Arrays.copyOfRange(array, 0, mid);
int[] right = Arrays.copyOfRange(array, mid, array.length);
return merge(sort(left), sort(right));
}
/**
* 归并排序——将两段排序好的数组结合成一个排序数组
*
* @param left
* @param right
* @return
*/
public static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length)/*左边数组已经取完,完全取右边数组的值即可*/
result[index] = right[j++];
else if (j >= right.length)/*右边数组已经取完,完全取左边数组的值即可*/
result[index] = left[i++];
else if (left[i] > right[j])/*左边数组的元素值大于右边数组,取右边数组的值*/
result[index] = right[j++];
else/*右边数组的元素值大于左边数组,取左边数组的值*/
result[index] = left[i++];
}
return result;
}
6. 快速排序
快速排序是对冒泡排序的一种改进,也是采用分治法的一个典型的应用。
首先任意选取一个数据(比如数组的第一个数)作为关键数据,我们称为基准数。将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序,也称为分区(partition)操作。
通过一趟快速排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数组变成有序序列。
public static void sort(int[] array, int start, int end) {
if (array.length < 1 || start < 0 || end >= array.length || start > end)
return ;
int zoneIndex = realSort(array, start, end);
if (zoneIndex>start) {
sort(array, start, zoneIndex-1);
}
if (zoneIndex<end) {
sort(array, zoneIndex+1, end);
}
}
public static int realSort(int[] array, int start, int end){
//我们这里随机选择一个数当基准数,然后将基准数和数组最后一位互换
int pivot = (int) (start + Math.random() * (end - start + 1));
int zoneIndex = start - 1;
swap(array, pivot, end);
for (int i = start; i <=end; i++) {
if (array[i]<=array[end]) {
zoneIndex++;
if (zoneIndex<i) {
swap(array, zoneIndex, i);
}
}
}
return zoneIndex;
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
7. 堆排序
二叉堆,是一个完全二叉树的结构,同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。在一个二叉堆中,根节点总是最大(或者最小)节点。堆排序算法就是抓住了这一特点,每次都取堆顶的元素,然后将剩余的元素重新调整为最大(最小)堆,依次类推,最终得到排序的序列。
对于完全二叉树,对于位置为K的结点 左子结点=2k+1 右子结点=2(k+1)。最后一个非叶节点的位置为 (N/2)-1,N为数组长度。
排序的规则简单来说就是从最后一个非叶节点开始,从下到上,从右到左调整。
public static void sort(int[] array) {
len = array.length;
if (len < 1) return ;
buildMaxHeap(array);
PrintArray.print(PrintArray.SRC);
while (len > 0) {
swap(array, 0, --len);
adjustHeap(array, 0);
}
}
public static void buildMaxHeap(int[] array) {
int index = len/2-1;
for (int i = index; i>=0; i--) {
adjustHeap(array, i);
}
}
public static void adjustHeap(int[] array, int i){
int left = 2*i+1;
int right = 2*i+2;
int max = i;
if (left < len &&array[left]>array[max]) {
max=left;
}
if (right < len &&array[right]>array[max]) {
max=right;
}
if (max != i) {
swap(array, max, i);
adjustHeap(array, max);
}
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
8. 桶排序
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,利用某种函数的映射关系将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序)。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做排序即可。
/**
*
* @param array
* @param bucketSize BucketSize,作为每个桶所能放置多少个不同数值
* (例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,
* 但是容量不限,即可以存放100个3);
* @return
*/
public static ArrayList<Integer> sort(ArrayList<Integer> array, int bucketSize) {
if (array == null || array.size() < 2)
return array;
int max = array.get(0), min = array.get(0);
// 找到最大值最小值
for (int i = 0; i < array.size(); i++) {
if (array.get(i) > max)
max = array.get(i);
if (array.get(i) < min)
min = array.get(i);
}
/*获得桶的数量*/
int bucketCount = (max - min) / bucketSize + 1;
/*构建桶*/
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
ArrayList<Integer> resultArr = new ArrayList<>();
for (int i = 0; i < bucketCount; i++) {
bucketArr.add(new ArrayList<Integer>());
}
/*将原始数组中的数据分配到桶中*/
for (int i = 0; i < array.size(); i++) {
bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
}
/*看看桶中数据的分布*/
for (int i = 0; i < bucketArr.size(); i++) {
System.out.print("第"+i+"个桶包含数据:");
PrintArray.printObject(bucketArr.get(i));
}
for (int i = 0; i < bucketCount; i++) {
if (bucketSize == 1) {
for (int j = 0; j < bucketArr.get(i).size(); j++)
resultArr.add(bucketArr.get(i).get(j));
} else {
if (bucketCount == 1)
bucketSize--;
/*对桶中的数据再次用桶进行排序*/
ArrayList<Integer> temp = sort(bucketArr.get(i), bucketSize);
for (int j = 0; j < temp.size(); j++)
resultArr.add(temp.get(j));
}
}
return resultArr;
}
9. 总结
算法的稳定性:
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。
算法的复杂度:
时间复杂度: 一个算法执行所耗费的时间。
空间复杂度:对一个算法在运行过程中临时占用存储空间大小的量度。
常见复杂度由小到大:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n)
时间复杂度记忆:
冒泡、选择、插入排序需要两个for循环,每次只关注一个元素,平均时间复杂度为
(一遍找元素O(n),一遍找位置O(n))
快速、归并、堆基于分治思想,log以2为底,平均时间复杂度往往和O(nlogn)(一遍找元素O(n),一遍找位置O(logn))相关
而希尔排序依赖于所取增量序列的性质,但是到目前为止还没有一个最好的增量序列 。例如希尔增量序列时间复杂度为O(n²),而Hibbard增量序列的希尔排序的时间复杂度为 , 有人在大量的实验后得出结论;当n在某个特定的范围后希尔排序的最小时间复杂度大约为n^1.3。
从平均时间来看,快速排序是效率最高:
快速排序中平均时间复杂度O(nlog n),这个公式中隐含的常数因子很小,比归并排序的O(nlog n)中的要小很多,所以大多数情况下,快速排序总是优于合并排序的。
而堆排序的平均时间复杂度也是O(nlog n),但是堆排序存在着重建堆的过程,它把根节点移除后,把最后的叶子结点拿上来后需要重建堆,但是,拿上的值是要比它的两个叶子结点要差很多的,一般要比较很多次,才能回到合适的位置。堆排序就会有很多的时间耗在堆调整上。
虽然快速排序的最坏情况为排序规模(n)的平方关系,但是这种最坏情况取决于每次选择的基准, 对于这种情况,已经提出了很多优化的方法,比如三取样划分和Dual-Pivot快排。同时,当排序规模较小时,划分的平衡性容易被打破,而且频繁的方法调用超过了O(nlog n)为省出的时间,所以一般排序规模较小时,会改用插入排序或者其他排序算法。