首先我们了解下什么是冒泡排序:
介绍
冒泡排序属于一种简单的排序,也是经典的一种排序思想。
原理:比较两个相邻的元素,将值大或小的元素交换至右端(相邻位置作交换)。
思路:依次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第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) 。
当有N个数需要排列时,按照冒泡排序的思想,需要进行N-1的排序,每次排序将最大或最小的数放到数组的尾部(每次内部排序完后那么下次只需比较除最后一个数外的数组的其他的数,因为最后一个数已经符合最大或最小的要求)。
代码演示:
@Test
public void test1() {
int[] arr = new int[] { 3,1,4,2,5,7,6 };
for (int i = 0; i < arr.length - 1; i++) {//排序总次数为i*j,外循环一次代表从第一位数到最后一位排序一趟
for (int j = 0; j < arr.length - 1 - i; j++) {//内循环一次代表相邻两个数比较一次
if (arr[j] < arr[j + 1]) {//判断当前下标的数据是否小于下一个下标位置的数据
int temp = arr[j];//temp为空容器,用来存储当前下标的数据
arr[j] = arr[j + 1];//把当前下标的数据覆盖掉
arr[j + 1] = temp;//把下一个下标的数据覆盖掉
}
}
}
for (int i : arr) {//foreach遍历
System.out.print(i + " ");
}
}
优化后的:
@Test
public void test1() {
/*
* 每次减少一趟循环,若内循环一趟下来没有进行排序,说明全部都是有序的,就结束外层循环
*/
int[] arr = new int[] { 3,1,4,2,5,7,6 };
boolean flag = true;//标记
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] < arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = false;//未排序完成标记
}
}
if (flag) {//若一趟下来,flag未重新被赋值为false,说明排序完成,数据序列为有序,就提前结束外循环
break;
}
}
for (int i : arr) {//foreach遍历
System.out.print(i + " ");
}
}
快速排序:
提示:上图中红色柱子为基准,小的放在左边,大的放在右边。
介绍
快速排序(Quicksort)是对冒泡排序的一种改进。
快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
原理:
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
代码演示:
/**
* 快速排序 (固定基准快排)
* 采用分治策略(正序与倒序)
*/
@Test
public void test2() {
int[] arr = new int[] { 1, 4, 5, 2, 8, 9, 10, 20, 13, 17 };
sort_desc(arr, 0, arr.length - 1);
System.out.println("降序排序后:"+Arrays.toString(arr));
sort_asc(arr, 0, arr.length - 1);
System.out.println("正序排序后:"+Arrays.toString(arr));
}
/*
* 正序
*/
public void sort_asc(int[] arr,int low,int high){//传入待排数据,low是基准位置,high是最大长度
checks(arr, low, high);//数据检测
int temp = 0;//默认的空容器
int start = low;//把基准位置定为排序的头部位置的下标,代表最左边的下标
int end = high;//把最大长度作为最右边的下标,尾部下标
int standradVal = arr[start];//初始基准数据为基准位置的数据
while(end > start){//退出条件low>=high
while(end > start && arr[end] > standradVal )//从后向前排,若后边的数据大于基准数据,满足正序的数据格式,就把右边的下标向前移动,直到找到比基准位置数据小的数据
end --;
if(arr[end] < standradVal){//交换右边比左边数据小的数据
swap(arr, end, start, temp);//调用swap交换方法
}
while(end > start && arr[start] < standradVal )//从前向后排,若前边的数据大于基准数据,就把右边的下标向后移动,直到找到比基准位置数据大的数据
start ++;
if(arr[start] > standradVal){//交换
swap(arr, end, start, temp);
}
}
/*
* 采用递归的形式完成
* 只要头部发生移动,说明还有数据发生排序,未排序完成,尾部也是同理
*/
if(end < high){//若结束时的尾部下标位置小于(不等于)初始最大位置的下标
sort_asc(arr,end+1,high);
}
if(start > low){//若结束时的头部下标位置大于(不等于)初始位置的下标
sort_asc(arr,low,start - 1);
}
}
/*
* 倒序与正序同理
*/
public void sort_desc(int[] arr,int low,int high){
checks(arr, low, high);
int temp = 0;
int start = low;
int end = high;
int standradVal = arr[start];
while(end > start){//退出条件low>=high
while(end > start && arr[end] < standradVal )
end --;
if(arr[end] > standradVal){
swap(arr, end, start, temp);
}
while(end > start && arr[start] > standradVal )
start ++;
if(arr[start] < standradVal){
swap(arr, end, start, temp);
}
}
if(end < high){
sort_desc(arr,end+1,high);
}
if(start > low){
sort_desc(arr,low,start - 1);
}
}
public void checks(int[] arr, int low, int high) {//检测传入数据是否合法,避免报错
if (arr.length - 1 <= 0 || arr == null || low < 0 || high > arr.length - 1) {
return;//不合法就返回
}
}
public void swap(int[] arr, int end, int start, int temp) {//交换的方法
temp = arr[end];//每次交换之后基准位置就会变成start或end,因为从后向前排序,end为基准右边需要交换的数据
arr[end] = arr[start];//start为左边需要交换的数据,覆盖原有数据
arr[start] = temp;//最后把temp容器中的数据覆盖原有数据
}
这个是固定基准(默认从0开始,开始时以下标为0的数为基准);
它的时间复杂度为:
快速排序具有最好的平均性能(average behavior),但最坏性能(worst case behavior)和插入排序
相同,也是O(n^2)。比如一个序列5,4,3,2,1,要排为1,2,3,4,5。按照快速排序方法,每次只会有一个数据进入正确顺序,不能把数据分成大小相当的两份,很明显,排序的过程就成了一个歪脖子树,树的深度为n,那时间复杂度就成了O(n^2)。尽管如此,需要排序的情况几乎都是乱序的,自然性能就保证了。据书上的测试图来看,在数据量小于20的时候,插入排序具有最好的性能。当大于20时,快速排序具有最好的性能,归并(merge sort)和堆排序(heap sort)也望尘莫及,尽管复杂度都为nlog2(n)。
其他优化快排:
随机化快排(比较常用)
快速排序的最坏情况基于每次划分对主元的选择。基本的快速排序选取第一个元素作为主元。这样在数组已经有序的情况下,每次划分将得到最坏的结果。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。一位前辈做出了一个精辟的总结:“随机化快速排序可以满足一个人一辈子的人品需求。”
随机化快速排序的唯一缺点在于,一旦输入数据中有很多的相同数据,随机化的效果将直接减弱。对于极限情况,即对于n个相同的数排序,随机化快速排序的时间复杂度将毫无疑问的降低到O(n^2)。解决方法是用一种方法进行扫描,使没有交换的情况下主元保留在原位置。
平衡快排
每次尽可能地选择一个能够代表中值的元素作为关键数据,然后遵循普通快排的原则进行比较、替换和递归。通常来说,选择这个数据的方法是取开头、结尾、中间3个数据,通过比较选出其中的中值。取这3个值的好处是在实际问题中,出现近似顺序数据或逆序数据的概率较大,此时中间数据必然成为中值,而也是事实上的近似中值。万一遇到正好中间大两边小(或反之)的数据,取的值都接近最值,那么由于至少能将两部分分开,实际效率也会有2倍左右的增加,而且利于将数据略微打乱,破坏退化的结构。
外部快排
与普通快排不同的是,关键数据是一段buffer,首先将之前和之后的M/2个元素读入buffer并对该buffer中的这些元素进行排序,然后从被排序数组的开头(或者结尾)读入下一个元素,假如这个元素小于buffer中最小的元素,把它写到最开头的空位上;假如这个元素大于buffer中最大的元素,则写到最后的空位上;否则把buffer中最大或者最小的元素写入数组,并把这个元素放在buffer里。保持最大值低于这些关键数据,最小值高于这些关键数据,从而避免对已经有序的中间的数据进行重排。完成后,数组的中间空位必然空出,把这个buffer写入数组中间空位。然后递归地对外部更小的部分,循环地对其他部分进行排序。
三路基数快排
(Three-way Radix Quicksort,也称作Multikey Quicksort、Multi-key Quicksort):结合了基数排序(radix sort,如一般的字符串比较排序就是基数排序)和快排的特点,是字符串排序中比较高效的算法。该算法被排序数组的元素具有一个特点,即multikey,如一个字符串,每个字母可以看作是一个key。算法每次在被排序数组中任意选择一个元素作为关键数据,首先仅考虑这个元素的第一个key(字母),然后把其他元素通过key的比较分成小于、等于、大于关键数据的三个部分。然后递归地基于这一个key位置对“小于”和“大于”部分进行排序,基于下一个key对“等于”部分进行排序。
二分法:
介绍
基本思想:假设数据是按升序排序的,对于给定值key,从序列的中间位置k开始比较,
如果当前位置arr[k]值等于key,则查找成功;若key小于当前位置值arr[k],则在数列的前半段中查找,arr[low,mid-1];
若key大于当前位置值arr[k],则在数列的后半段中继续查找arr[mid+1,high],直到找到为止,时间复杂度:O(log(n))
二分法又叫折半法(二分查找,折半查找),是一种二叉树排序或查找思想,非常适合大数据的操作,类似于上面的快速排序思想,采用基准位置来划分左右数据。
算法:当数据量很大适宜采用该方法。采用二分法查找时,数据需是有序不重复的。 基本思想:假设数据是按升序排序的,对于给定值 x,从序列的中间位置开始比较,如果当前位置值等于 x,则查找成功;若 x 小于当前位置值,则在数列的前半段中查找;若 x 大于当前位置值则在数列的后半段中继续查找,直到找到为止。
假设有一个数组 { 12, 23, 34, 45, 56, 67, 77, 89, 90 },现要求采用二分法找出指定的数值并将其在数组的索引返回,如果没有找到则返回 -1。代码如下:
代码演示:
public class DichotomySearch {
public static void main(String[] args) {
int[] arr = new int[] { 12, 23, 34, 45, 56, 67, 77, 89, 90 };
System.out.println(search(arr, 12));
}
public static int search(int[] arr, int key) {
int start = 0;
int end = arr.length - 1;
while (start <= end) {
int middle = (start + end) / 2;
if (key < arr[middle]) {
end = middle - 1;
} else if (key > arr[middle]) {
start = middle + 1;
} else {
return middle;
}
}
return -1;
}
}
时间复杂度:
查找数据长度为N,每次查找后减半,
第一次 N/2
...
第k次 N/2^k
最坏的情况下第k次才找到,此时只剩一个数据,长度为1。
即 N/2^k = 1
查找次数 k=log(N)。
总结:
快速排序、冒泡排序都是比较简单的排序方法,是我们学习更高级排序算法或思想的基础;二分查找(折半查找)是我们经常在实际开发中遇到的一种快速查找定位数据的思想或方案,数据结构的优化等。