排序算法总结
排序需要两个基本方法:判断两个元素大小,交换两个元素的位置,把这两种方法写在模板类中
public abstract class Sort<T extends Comparable<T>> { public abstract void sort(T[] nums); protected boolean less(T v, T w){ return v.compareTo(w)<0; } protected void swap(T[] a, int i, int j){ T temp = a[i]; a[i] = a[j]; a[j] = temp; } }
1.选择排序:从数组第0位开始,逐个和后边的元素比较,如果后边的元素比较小,就把两者替换,直到遍历完数组,可以找到数组中的最小元素。再从第1位开始,重复这个过程,直到数组最后一位。时间复杂度:log(N2)
public class SelectSort<T extends Comparable<T>> extends Sort<T> { @Override public void sort(T[] nums) { int n = nums.length; for(int i = 0; i<n; i++){ for(int j = i+1; j<n; j++){ if(less(nums[j], nums[i])) swap(nums,i,j); } } } }
2.冒泡排序:数组中相邻的两个元素比较,把较大元素移到后边,数组中最大的元素就像气泡一位一位地上浮到最后边。选择排序是先找到最小值并放在前边,冒泡排序是先找到最大值并放在后边。时间复杂度:log(N2)
public class BubbleSort<T extends Comparable<T>>extends Sort<T> { @Override public void sort(T[] nums) { int n = nums.length; boolean isSorted = false; //表示一次遍历中是否发生过交换,如果没有,说明已经排好序了,没必要继续往后算,否则说明数组还没排好序 for(int i = n-1; i>=0 && !isSorted; i--){ isSorted = true; for(int j = 0; j<i; j++){ if(less(nums[j+1],nums[j])) isSorted = false; swap(nums, j, j+1); } } } }
3.插入排序:可以理解为倒过来的冒泡排序,把数组分成左右两部分,左数组的最后一位与前一位比较,如果比前面小就交换位置,使最小值一位一位的被移动到前边。一轮比较完成后,左数组向右扩大一位,直到左数组大小等于原始数组。但是插入排序与冒泡排序不同的是,冒泡排序没有利用之前已经取得的成果,也就是说最大值上浮到右边之后就再也没动作,但是插入排序的左数组一直在参与排序,并且在一轮比较完成后,左数组一定是有序的,所以下一轮开始前,如果新的左数组最后一位比前一位大,说明它已经是最大值了,不需要继续比较,所以减小了时间复杂度。最坏的情况是数组是倒序的,需要 ~N2/2 比较以及 ~N2/2 次交换。最好的情况就是数组已经有序了,需要 N-1 次比较和 0 次交换。
public class Insertion<T extends Comparable<T>> extends Sort<T> { @Override public void sort(T[] nums) { int N = nums.length; for (int i = 1; i < N; i++) { for (int j = i; j > 0 && less(nums[j], nums[j - 1]); j--) { swap(nums, j, j - 1); } } } }
4.希尔排序:插入排序的改进。排序的本质是使数组中的逆序对数目减小到0,插入排序每次交换只能使逆序对数目减少1,因为交换的两个数的位置是相邻的,希尔排序增大了要交换的两个数之间的距离,从而增加了每次减少的逆序对数目。
public class Shell<T extends Comparable<T>> extends Sort<T> { @Override public void sort(T[] nums) { int N = nums.length; int h = 1; while (h < N / 3) { h = 3 * h + 1; // 1, 4, 13, 40, ... } while (h >= 1) { for (int i = h; i < N; i++) { for (int j = i; j >= h && less(nums[j], nums[j - h]); j -= h) { swap(nums, j, j - h); } } h = h / 3; } } }
5.快速排序
快速排序的思想是,把数组的第一个值放到正确的位置,使这个值左边的数字都比它小,右边的数字都比它大,因此把数组分成了两个部分,再用递归对这两个部分排序。
那么如何把第一个值放到正确的位置呢?需要用双指针 i 和 j ,i 始于待比较数字的下一个位置, j 始于数组尾部,对数组nums[] 来说,如果nums[i] > nums[0] 且 nums[j] < nums[0], 那么nums[i] 和 nums[j] 交换位置,直到i >= j。
public class QuickSort<T extends Comparable<T>> extends Sort<T> { @Override public void sort(T[] nums) { shuffle(nums); // 打乱数组,因为如果数组本来是倒序的话,时间复杂度会用O(NlogN)增加为O(N2) sort(nums, 0, nums.length - 1); } private void sort(T[] nums, int l, int h) { if (h <= l) return; int j = partition(nums, l, h); sort(nums, l, j - 1); sort(nums, j + 1, h); } private void shuffle(T[] nums) { List<Comparable> list = Arrays.asList(nums); Collections.shuffle(list); list.toArray(nums); } private int partition(T[] nums, int l, int h) { int i = l, j = h + 1; T v = nums[l]; while (true) { while (less(nums[++i], v) && i != h) ; while (less(v, nums[--j]) && j != l) ; if (i >= j) break; swap(nums, i, j); } swap(nums, l, j); return j; } }
6.归并排序
快速排序是先排序再切分,归并排序是先切分再排序,归并的意思是,对于两个已经排序的数组,如果要把这两个数组放在一起排序,只需要从两个数组起始位置开始比较,把较小的元素放进新数组,在把对应的指针后移,重复这个过程。那么怎么得到这两个排序的数组呢?首先把数组从中间分开,得到两个数组,然后分别对它们俩排序,所以最后问题从对大数组排序变成了对小数组排序,很明显是个递归问题。
public class Up2DownMergeSort<T extends Comparable<T>> extends MergeSort<T> { private int[] aux; // 辅助数组 @Override public void sort(T[] nums) { aux = (T[]) new Comparable[nums.length]; sort(nums, 0, nums.length - 1); } private void sort(T[] nums, int l, int h) { if (h <= l) { return; } int mid = l + (h - l) / 2; sort(nums, l, mid); sort(nums, mid + 1, h); merge(nums, l, mid, h); } protected void merge(T[] nums, int l, int m, int h) { int i = l, j = m + 1; for (int k = l; k <= h; k++) { aux[k] = nums[k]; // 将数据复制到辅助数组 } for (int k = l; k <= h; k++) { if (i > m) { nums[k] = aux[j++]; } else if (j > h) { nums[k] = aux[i++]; } else if (aux[i].compareTo(aux[j]) <= 0) { nums[k] = aux[i++]; } else { nums[k] = aux[j++]; } } } }
浙公网安备 33010602011771号