java数据结构(二)常用简单的查找、排序算法
理解了Java数据结构,还必须要掌握一些常见的基本算法。 理解算法之前必须要先理解的几个算法的概念:
空间复杂度:一句来理解就是,此算法在规模为n的情况下额外消耗的储存空间。
时间复杂度:一句来理解就是,此算法在规模为n的情况下,一个算法中的语句执行次数称为语句频度或时间频度。
稳定性:主要是来描述算法,每次执行完,得到的结果都是一样的,但是可以不同的顺序输入,可能消耗的时间复杂度和空间复杂度不一样。
一、二分查找算法
二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好,占用系统内存较少;其缺点是要求待查表为有序表,且插入删除困难。这个是基础,最简单的查找算法了。
public static voidmain(String[] args) {int srcArray[] = {3, 5, 11, 17, 21, 23, 28, 30, 32, 50, 64, 78, 81, 95, 101};
System.out.println(binSearch(srcArray,95));
}/*** 二分查找针对的是有序的数组
* 二分查找普通循环实现
*
*@paramsrcArray 有序数组
*@paramkey 查找元素
*@return
*/
public static int binSearch(int srcArray[], intkey) {if (srcArray.length <= 0) {return -1;
}//取出首尾判断key是否在指定的srcArray中
int first = srcArray[0];int end = srcArray[srcArray.length - 1];if (key <= first || key >=end) {return -1;
}int mid = srcArray.length / 2;if (key ==srcArray[mid]) {returnmid;
}//二分核心逻辑
int startIndex = 0;int endIndex = srcArray.length - 1;while (startIndex <=endIndex) {
mid= (endIndex - startIndex) / 2 +startIndex;if (key
endIndex= mid - 1;
}else if (key >srcArray[mid]) {
startIndex= mid + 1;
}else{returnmid;
}
}return -1;
}
二分查找算法如果没有用到递归方法的话,只会影响CPU。对内存模型来说影响不大。时间复杂度log2n,2的开方。空间复杂度是2。一定要牢记这个算法。应用的地方也是非常广泛,平衡树里面大量采用。
二、递归算法
递归简单理解就是方法自身调用自身。
public static void main(String[] args) {
int srcArray[] = {3,5,11,17,21,23,28,30,32,50,64,78,81,95,101};
System.out.println(binSearch(srcArray, 0,15,28));
}
/**
* 二分查找递归实现
*
* @param srcArray 有序数组
* @param start 数组低地址下标
* @param end 数组高地址下标
* @param key 查找元素
* @return 查找元素不存在返回-1
*/
public static int binSearch(int srcArray[], int start, int end, int key) {
int mid = (end - start) / 2 + start;
if (srcArray[mid] == key) {
return mid;
}
if (start >= end) {
return -1;
} else if (key > srcArray[mid]) {
return binSearch(srcArray, mid + 1, end, key);
} else if (key < srcArray[mid]) {
return binSearch(srcArray, start, mid - 1, key);
}
return -1;
}
递归几乎会经常用到,需要注意的一点是:递归不光影响的CPU。JVM里面的线程栈空间也会变大。所以当递归的调用链长的时候需要-Xss设置线程栈的大小。
三、八大排序算法
一、直接插入排序(Insertion Sort)
二、希尔排序(Shell Sort)
三、选择排序(Selection Sort)
四、堆排序(Heap Sort)
五、冒泡排序(Bubble Sort)
六、快速排序(Quick Sort)
七、归并排序(Merging Sort)
八、基数排序(Radix Sort)
1:冒泡排序
基本思想:
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
以下是冒泡排序算法复杂度:
平均时间复杂度最好情况最坏情况空间复杂度
O(n²)
O(n)
O(n²)
O(1)
冒泡排序是最容易实现的排序, 最坏的情况是每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²). 最佳的情况是内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n). 平均来讲, 时间复杂度为O(n²). 由于冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1).
由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法.
/**
* 冒泡排序普通版
* @param arr 待排序的数组
* @param len 数组中的元素个数
*/
public static void sort(Integer [] arr, int len){
if(len <= 1){
return;
}
// 数组中有len个元素,进行len次冒泡
for (int i = 0; i < len; i++) {
for (int j = 0; j < len - i - 1; j++) {
if(arr[j] > arr[j + 1]){
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
如果说在一次冒泡中,没有发生相邻元素的交换,那说明待排序序列已经有序了,不管后面还剩下多少次冒泡,我们都不需要再进行冒泡下去了。这样是不是就减少冒泡的次数了呢
改良版:
/**
* 冒泡排序改良版
* @param arr 待排序的数组
* @param len 数组中的元素个数
*/
public static void sort(Integer [] arr, int len){
if(len <= 1){
return;
}
// 优化标识
// 如果在某一次的冒泡过程中,没有位置交换说明已经排好序,直接break
boolean flag = false;
// 数组中有len个元素,进行len次冒泡
for (int i = 0; i < len; i++) {
// 如果有位置交换就重置标识为false
flag = false;
for (int j = 0; j < len - i - 1; j++) {
if(arr[j] > arr[j + 1]){
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = true;
}
}
// 每次冒泡结束检查是否发生了数据交换
// 如果没有发生数据交换,说明序列已经有序,不需要再继续冒泡了
System.out.println("第【" + (i + 1) + "】次冒泡");
if(!flag){
break;
}
}
}
2:快速排序
快速排序使用分治策略来把一个序列(list)分为两个子序列(sub-lists)。步骤为:
①. 从数列中挑出一个元素,称为”基准”(pivot)。
②. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
③. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
代码实现:
用伪代码描述如下:
①. i = L; j = R; 将基准数挖出形成第一个坑a[i]。
②.j--,由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
③.i++,由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
④.再重复执行②,③二步,直到i==j,将基准数填入a[i]中。
快速排序采用“分而治之、各个击破”的观念,此为原地(In-place)分区版本。
/**
* 快速排序(递归)
*
* ①. 从数列中挑出一个元素,称为"基准"(pivot)。
* ②. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
* ③. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
* @param arr 待排序数组
* @param low 左边界
* @param high 右边界
*/
public static void quickSort(int[] arr, int low, int high){
if(arr.length <= 0) return;
if(low >= high) return;
int left = low;
int right = high;
int temp = arr[left]; //挖坑1:保存基准的值
while (left < right){
while(left < right && arr[right] >= temp){ //坑2:从后向前找到比基准小的元素,插入到基准位置坑1中
right--;
}
arr[left] = arr[right];
while(left < right && arr[left] <= temp){ //坑3:从前往后找到比基准大的元素,放到刚才挖的坑2中
left++;
}
arr[right] = arr[left];
}
arr[left] = temp; //基准值填补到坑3中,准备分治递归快排
System.out.println("Sorting: " + Arrays.toString(arr));
quickSort(arr, low, left-1);
quickSort(arr, left+1, high);
}
以下是快速排序算法复杂度:
平均时间复杂度最好情况最坏情况空间复杂度
O(nlog₂n)
O(nlog₂n)
O(n²)
O(1)(原地分区递归版)
快速排序排序效率非常高。 虽然它运行最糟糕时将达到O(n²)的时间复杂度, 但通常平均来看, 它的时间复杂为O(nlogn), 比同样为O(nlogn)时间复杂度的归并排序还要快. 快速排序似乎更偏爱乱序的数列, 越是乱序的数列, 它相比其他排序而言, 相对效率更高.