排序
冒泡排序
介绍:
冒泡排序的原理非常简单,它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
排序原理:
- 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较为止。
实现:
public class Bubble {
public static void main(String[] args) {
int[] arr = new int[]{43, 32, 76, -98, 0, 64, 33, -21, 32, 99};
bubbleSort(arr);
for (int i = 0; i < arr.length; i++) {//遍历输出
System.out.print(arr[i] + "\t");
}
}
/**
* 冒泡排序
* @param array 需要排序的数组
*/
static void bubbleSort(int[] array) {
if (array.length <= 1) {
return;
}
int temp;
for (int i = array.length - 1; i > 0; i--) {//外循环,元素个数-1 次
for (int j = 0; j < i; j++) {//内循环,交换相邻的两个元素
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
}
时间复杂度
这个时间复杂度还是很好计算的:外循环和内循环以及判断和交换元素的时间开销;
最优的情况也就是开始就已经排序好序了,那么就可以不用交换元素了,则时间花销为:[ n(n-1) ] / 2;所以最优的情况时间复杂度为:O( n^2 );
最差的情况也就是开始的时候元素是逆序的,那么每一次排序都要交换两个元素,则时间花销为:[ 3n(n-1) ] / 2;(其中比上面最优的情况所花的时间就是在于交换元素的三个步骤);所以最差的情况下时间复杂度为:O( n^2 );
综上所述:
最优的时间复杂度为:O( n^2 ) ;有的说 O(n),下面会分析这种情况;
最差的时间复杂度为:O( n^2 );
平均的时间复杂度为:O( n^2 );
空间复杂度
空间复杂度就是在交换元素时那个临时变量所占的内存空间;
最优的空间复杂度就是开始元素顺序已经排好了,则空间复杂度为:0;
最差的空间复杂度就是开始元素逆序排序了,则空间复杂度为:O(n);
平均的空间复杂度为:O(1);
空间复杂度优化
有人会说这个空间复杂度能降到 0,因为空间复杂度主要是看使用的辅助内存,如果没有辅助内存变量,那么可以说空间复杂度为0;所以该算法中空间复杂度一般是看交换元素时所使用的辅助空间;
a = a + b; b = a - b; a = a - b;
a = a * b; b = a / b; a = a / b;
a = a ^ b; b = a ^ b;a = a ^ b;
上面几种方法都可以不使用临时空间来交换两个元素,但是都有些潜在的问题,比如 越界。
快速排序
原理:
快速排序是从冒泡排序中演变而来的算法,但比冒泡排序要高效很多,因为使用了分治的思想。
同冒泡排序一样,快速排序也属于交换排序,通过元素间的比较和位置的交换来达到排序的目的。 不同的是,冒泡排序在每一轮只把一个元素冒泡到散列的一端。而快速排序每一轮挑选一个“基准”元素,并让其他比他大的元素移动到数列一边,比他小的元素移动到数列另一边,从而把数列拆解成两个部分。
如图所示,在分治法的思想下,原数列在每一轮被拆分成两部分,每一部分在下一轮又分别被拆分成两部分,直到不可再分为止。
这样一共需要多少轮呢?平均情况下需要 logn 轮,因此快速排序算法的平均时间复杂度是 O(nlogn)
。
基准元素(pivot)的选取:
移动元素有两种方式:指针交换法、挖坑法
指针交换法:
假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。然后在这个队列找一个数作为基准,我们选择左边第一个数,即为6,那么需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边。
方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量 i 和 j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。
首先哨兵 j 开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j- -),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
能不能在等于6的数停下来呢?不能,造成麻烦
到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:
第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下:
到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。
OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。这样循环下去直至序列不可再分。也就排序完成了。
下图作个总结:
代码实现:
package sort;
public class QuickSort {
public static void main(String[] args) {
int[] arr = {6,1,6,2,6,7,9,6,11,6,4,5,6,10};
quickSort(arr, 0, arr.length - 1);
for (int i : arr) {
System.out.print(i + "\t");
}
}
/**
* 快速排序 时间复杂度 O(nlogn)
* @param arr 要排序的数组
* @param start 要排序的数组的起始索引,包括此元素
* @param end 要排序的数组的终止索引,包括此元素
*/
public static void quickSort(int[] arr, int start, int end) {
if (start >= end) {
return;
}
int i, j, temp, t;
i = start;
j = end;
temp = arr[start];//基准,位置在左边
//移动指针,进行分区
while (i < j) {
//先看右边,依次往左递减。为什么要先看右边呢,因为基准位置在左边,我们要保证两指针相遇的位置上的元素比基准小。
while (temp <= arr[j] && i < j) j--;
//再看左边,依次往右递增
while (temp >= arr[i] && i < j) i++;
//如果满足条件,互换左右元素
if (i < j) {
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
//此时i和j重合,该位置也是基准适当的位置,将这两个位置的元素互换
arr[start] = arr[i];
arr[i] = temp;
//递归分区后的两数组
quickSort(arr, start, i - 1);
quickSort(arr, i + 1, end);
}
}
挖坑法:
代码实现:
/**
* 快排 挖坑法
* @param arr 要排序的数组
* @param start 要排序的数组的起始索引,包括此元素
* @param end 要排序的数组的终止索引,包括此元素
*/
public static void quickSort2(int[] arr, int start, int end) {
if (start >= end) return;//终止递归条件
int left = start;//左指针
int right = end;//右指针
int pivot = arr[start];//基准
int index = left;//坑的位置,初始为左指针的位置
while (left < right) {//开始这轮指针移动
while (arr[right] >= pivot && left < right) right--;//移动右指针,当遇到比基准小的数停下来
arr[index] = arr[right];//将数填入坑中
index = right;//更新坑的位置,为右指针的位置
while (arr[left] <= pivot && left < right) left++;//移动左指针,当遇到比基准大的数停下来
arr[index] = arr[left];//将数填入坑中
index = left;//更新坑的位置,为左指针的位置
}
arr[index] = pivot;//这一轮完毕,将基准填入坑中,此时坑的位置即为基准的适当位置
quickSort2(arr, start, index - 1);//递归左边分区的序列
quickSort2(arr, index + 1, end);//递归右边分区的序列
}
查找
二分法查找
前提:所要查找的数组必须有序
原理:从数组中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组已经为空,则表示找不到指定的元素。二分法查找每次是使查找范围缩小一半,其时间复杂度是O(logn)。
代码实现:
public class BinarySearch {
public static void main(String[] args) {
int[] arr = new int[]{-98,-34,2,34,54,66,79,105,210,333};
System.out.println(binarySearch(arr, 100, 0, arr.length - 1));
}
/**
* 二分法查找,时间复杂度 O(logn),返回目标元素索引,找不到则返回-1
* @param arr 目标数组
* @param dest 目标元素
* @param start 起始搜索索引 包含此元素
* @param end 终止索引 包含此元素
* @return
*/
public static int binarySearch(int[] arr, int dest, int start, int end) {
int middle;
while (start <= end) {//开始二分
middle = (end + start) / 2;//中间点
if (dest == arr[middle]) {//如果该中间点为目标元素,则返回
return middle;
} else if (dest > arr[middle]) start = middle + 1;//如果目标元素比中间点大,则继续找右边
else end = middle - 1;//反之则继续找左边
}
return -1;//找不到返回-1
}
}
``