冒泡排序
原理:比较两个相邻的元素,将值大的元素交换至右端。
思路:依次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。重复第一趟步骤,直至全部排序完成。
冒泡排序的优点:每进行一趟排序,就会少比较一次,因为每进行一趟排序都会找出一个较大值。
用时间复杂度来说:
1.如果我们的数据正序,只需要走一趟即可完成排序。所需的比较次数C和记录移动次数M均达到最小值,即:Cmin=n-1;Mmin=0;所以,冒泡排序最好的时间复杂度为O(n)。
2.如果很不幸我们的数据是反序的,则需要进行n-1趟排序。每趟排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
冒泡排序的最坏时间复杂度为:O(n^2) 。
综上所述:冒泡排序总的平均时间复杂度为:O(n^2) 。
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {2, 1, 8, 5, 6,3};
System.out.print("排序之前: ");
System.out.println(Arrays.toString(arr));
int length = arr.length;
for(int i = 0; i < length - 1; i++) {
for(int j = 0; j < length - i - 1; j++) {
if(arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
System.out.print("排序后: ");
System.out.println(Arrays.toString(arr));
}
}
鸡尾酒排序
鸡尾酒排序的概念:鸡尾酒排序又叫定向冒泡排序,鸡尾酒搅拌排序,搅拌排序(也可以视作选择排序的一种变形),涟漪排序,来回排序或快乐小时排序,是冒泡排序的一种变形。此算法与冒泡排序的不同处在于排序时是以双向在序列中进行排序。
鸡尾酒排序的算法描述如下:
先对数组从左到右进行升序的冒泡排序;
再对数组进行从右到左的降序的冒泡排序;
以此类推,持续的、依次的改变冒泡的方向,并不断缩小没有排序的数组范围;
鸡尾酒排序复杂度:
时间复杂度 O(n^2)
最优时间复杂度 O(n)
平均时间复杂度 O(n^2)
public class CocktailSort {
public static void main(String[] args) {
int[] arr = { 2, 1, 8, 5, 6, 3 };
System.out.print("排序之前: ");
System.out.println(Arrays.toString(arr));
int length = arr.length;
int left = 0;
int right = length - 1;
int i, tmp;
while (left < right) {
for (i = left; i < right; i++) {
if (arr[i] > arr[i + 1]) {
tmp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = tmp;
}
}
right--;
for(i = right; i > left; i--) {
if(arr[i] > arr[i + 1]) {
tmp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = tmp;
}
}
left++;
}
System.out.print("排序后: ");
System.out.println(Arrays.toString(arr));
}
}
选择排序
原理:每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。也就是:每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。
效率:对于长度为N的数组,选择排序需要大约N²/2次比较和N次交换。也即最好、最差、平均时间效率均为O(n²),只需要一个辅助变量帮助交换元素。
public class SelectionSort {
public static void main(String[] args) {
int[] arr = { 2, 1, 8, 5, 6, 3 };
System.out.print("排序之前: ");
System.out.println(Arrays.toString(arr));
int length = arr.length;
for (int i = 0; i < length; i++) {
int k = i;
for (int j = k + 1; j < length; j++) {
if(arr[j] < arr[k]) {
k = j;
}
}
if(i != k) {
int tmp = arr[i];
arr[i] = arr[k];
arr[k] = tmp;
}
System.out.println();
for(int a : arr) {
System.out.print(a);
}
}
System.out.print("排序后: ");
System.out.println(Arrays.toString(arr));
}
}
插入排序
插入排序类似整理扑克牌,将每一张牌插到其他已经有序的牌中适当的位置。
插入排序由N-1趟排序组成,对于P=1到N-1趟,插入排序保证从位置0到位置P上的元素为已排序状态。
简单的说,就是插入排序总共需要排序N-1趟,从index为1开始,讲该位置上的元素与之前的元素比较,放入合适的位置,这样循环下来之后,即为有序数组。
效率:如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)
public class InsertSort {
public static void main(String[] args) {
int[] arr = { 2, 1, 8, 5, 6, 3 };
System.out.print("排序之前: ");
System.out.println(Arrays.toString(arr));
int length = arr.length;
for(int i = 0; i < length - 1; i++) {
for(int j = i; j >= 0 && arr[j] > arr[j + 1];j--) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
System.out.print("排序后: ");
System.out.println(Arrays.toString(arr));
}
}
二分插入排序
二分插入排序的基本思想和插入排序一致;都是将某个元素插入到已经有序的序列的正确的位置; 和直接插入排序的最大区别是,元素A[i]的位置的方法不一样;直接插入排序是从A[i-1]往前一个个比较,从而找到正确的位置;而二分插入排序,利用前i-1个元素已经是有序的特点结合二分查找的特点,找到正确的位置,从而将A[i]插入,并保持新的序列依旧有序;
public class BinaryInsertSort {
public static void main(String[] args) {
int[] arr = { 2, 1, 8, 5, 6, 3 };
System.out.print("排序之前: ");
System.out.println(Arrays.toString(arr));
int length = arr.length;
int tmp;
for (int i = 1; i < length; i++) {
int left = 0;
int right = i - 1;
int mid;
while(left <= right) {
mid = (left + right) / 2;
if(arr[i] > arr[mid]) {
left = mid + 1;
}else {
right = mid - 1;
}
}
tmp = arr[i];
for(int j = i - 1; j > left -1; j--) {
arr[j + 1] = arr[j];
arr[j] = tmp;
}
}
System.out.print("排序后: ");
System.out.println(Arrays.toString(arr));
}
}
希尔排序
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。
增量序列{n/2,(n/2)/2…1}(希尔增量),其最坏时间复杂度依然为O(n2),一些经过优化的增量序列如Hibbard经过复杂证明可使得最坏时间复杂度为O(n3/2)。
public class ShellSort {
public static void main(String[] args) {
int[] arr = { 2, 1, 8, 5, 6, 3 };
System.out.print("排序之前: ");
System.out.println(Arrays.toString(arr));
int length = arr.length;
for(int gap = length / 2; gap > 0; gap /= 2) {
for(int i = gap; i < length; i++ ) {
int j = i;
while(j - gap >= 0 && arr[j] < arr[j - gap]) {
int tmp = arr[j];
arr[j] = arr[j - gap];
arr[j - gap] = tmp;
j -= gap;
}
}
}
System.out.print("排序后: ");
System.out.println(Arrays.toString(arr));
}
}
归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
public class MergeSort {
public static void main(String[] args) {
int[] arr = { 2, 1, 8, 5, 6, 3 };
System.out.print("排序之前: ");
System.out.println(Arrays.toString(arr));
sort(arr);
System.out.print("排序后: ");
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] arr) {
int [] tmp = new int[arr.length];
sort(arr, 0, arr.length - 1, tmp);
}
public static void sort(int[] arr, int left, int right, int[] tmp) {
if(left < right) {
int mid = (left + right) / 2;
sort(arr, left, mid, tmp);
sort(arr, mid + 1, right, tmp);
merge(arr, left, mid, right, tmp);
}
}
public static void merge(int[] arr, int left, int mid, int right, int[] tmp){
int i = left;
int j = mid + 1;
int t = 0;
while(i <= mid && j <= right) {
if(arr[i] < arr[j]) {
tmp[t++] = arr[i++];
}else {
tmp[t++] = arr[j++];
}
}
while(i <= mid) {
tmp[t++] = arr[i++];
}
while(j <= right) {
tmp[t++] = arr[j++];
}
t = 0;
while(left <= right) {
arr[left++] = tmp[t++];
}
}
}
堆排序
堆排序是把数组看作堆,第i个结点的孩子结点为第2i+1和2i+2个结点(不超出数组长度前提下),堆排序的第一步是建堆,然后是取堆顶元素然后调整堆。
建堆的过程是自底向上不断调整达成的,这样当调整某个结点时,其左节点和右结点已经是满足条件的,此时如果两个子结点不需要动,则整个子树不需要动,如果调整,则父结点交换到子结点位置,再以此结点继续调整。 使用的大顶堆,建立好堆后堆顶元素为最大值,此时取堆顶元素即使堆顶元素和最后一个元素交换,最大的元素处于数组最后,此时调整小了一个长度的堆,然后再取堆顶和倒数第二个元素交换,依次类推,完成数据的非递减排序。 堆排序的主要时间花在初始建堆期间,建好堆后,堆这种数据结构以及它奇妙的特征,使得找到数列中最大的数字这样的操作只需要O(1)的时间复杂度,维护需要logn的时间复杂度。
快速排序
快速排序是冒泡排序的改进版,也是最好的一种内排序,在很多面试题中都会出现,也是作为程序员必须掌握的一种排序方法。
思想:1.在待排序的元素任取一个元素作为基准(通常选第一个元素,但最的选择方法是从待排序元素中随机选取一个作为基准),称为基准元素;
2.将待排序的元素进行分区,比基准元素大的元素放在它的右边,比其小的放在它的左边;
3.对左右两个分区重复以上步骤直到所有元素都是有序的。
所以我是把快速排序联想成东拆西补或西拆东补,一边拆一边补,直到所有元素达到有序状态。
算法分析:
1.当分区选取的基准元素为待排序元素中的最大或最小值时,为最坏的情况,时间复杂度和直接插入排序的一样,移动次数达到最大值 Cmax = 1+2+…+(n-1) = n*(n-1)/2 = O(n2) 此时最好时间复杂为O(n2)
2.当分区选取的基准元素为待排序元素中的"中值",为最好的情况,时间复杂度为O(nlog2n)。
3.快速排序的空间复杂度为O(log2n).
4.当待排序元素类似[6,1,3,7,3]且基准元素为6时,经过分区,形成[1,3,3,6,7],两个3的相对位置发生了改变,所是快速排序是一种不稳定排序。
public class FastSort {
public static void main(String[] args) {
int[] arr = { 2, 1, 8, 5, 6, 3 };
System.out.print("排序之前: ");
System.out.println(Arrays.toString(arr));
int left = 0;
int right = arr.length - 1;
fast(arr, left, right);
System.out.print("排序后: ");
System.out.println(Arrays.toString(arr));
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
public static void fast(int[] arr,int left,int right) {
int i = left;
int j = right;
int tmp = 0;
if(left <= right) {
tmp = arr[left];
while(left != right) {
while(left < right && arr[right] >= tmp) {
right--;
}
arr[left] = arr[right];
while(left < right && arr[left] <= tmp) {
left++;
}
arr[right] = arr[left];
}
arr[right] = tmp;
fast(arr, i, left - 1);
fast(arr, right + 1, j);
}
}
}
比较
排序方法 平均情况 最好情况 最坏情况 稳定性
冒泡排序 O(n^2) O(n) O(n^2) 稳定
鸡尾酒排序 O(n^2) O(n) O(n^2)
选择排序 O(n^2) O(n^2) O(n^2) 不稳定
插入排序 O(n^2) O(n) O(n^2) 稳定
二分插入排序
希尔排序 O(nlogn)/O(n^2) O(n^1.3) O(n^2) 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) 稳定
堆排序 O(nlogn) O(nlogn) O(nlogn) 不稳定