准备蓝桥杯之路(四) ------ 简单的排序和查找入门
前言
今天本应该写的是递归方面的基础知识,可是在看视频的过程中我发现自己连最基础的排序和查找都没有完全掌握,俗话说的好,地基不牢,地动山摇,所以,我就又把基础的排序和查找在网上找出来重新温习了一遍,在此整理出来,和有需要的童鞋们一起学习,一起进步。
整理的查找算法有:循环查找(最最最基础的查找方法)和二分查找。
整理的排序算法有:冒泡排序、选择排序和插入排序,并对这三种排序算法的性能进行了简单的分析和比较。
系列文章链接:
准备蓝桥杯之路(一) ------ 为什么想要参加蓝桥杯?
准备蓝桥杯之路(二) ------ 位运算的奇淫技艺
准备蓝桥杯之路(三) ------ 位运算的奇淫技艺2
我是跟着哔哩哔哩的一个课程进行学习的,毕竟不是原创,所以下面我附上视频链接。
视频链接: 蓝桥杯零基础入门到比赛(二)——算法很美
1.查找
查找,顾名思义,用最简单的语言描述就是在一堆东西中找到自己想要的那件物品,而在编程中则被进一步具体化为:在一个有序或无序的数组中找到目标数字并返回其在数组中的下标值。下面所介绍的两种方法就是针对这一问题给出的两种不同的解决方案。
1.1 循环查找(该方法不存在任何技巧,非必要可以直接跳过)
算法思想描述: 这个应该是最最最简单的查找方法了,而且也是最贴近正常人思维的方法,它的思想就是从头开始遍历整个数组,并依次和目标数字进行比较,如果找到目标数字,则将其下标值返回,并跳出循环,否则,就返回-1表示数组中不包含目标数字。
代码实现
package Util;
public class Search {
/**
* 查找整形数组中目标值的下标
* 循环查找法 遍历数组中的所有元素去查找目标值的数组下标
* @param arr 整形数组
* @param target 查找目标值
* @return 目标值在数组中的下标(没有查到则返回 -1)
*/
public static int indexOf(int[] arr, int target){
int index = -1;
for(int i = 0; i < arr.length; i++){
if(arr[i] == target) {
index = i;
break;
}
}
return index;
}
}
1.2 二分查找
算法前提: 使用该算法的前提是待搜索数组应该是一个有序数组,否则将无法使用。
算法思想描述: 详解二分查找的优秀博客有很多,我会在下面帖出讲的较为详细的一篇文章供大家参考,接下来我就略微讲一下自己对二分查找的见解和思考。
对二分查找我有一个特别深且形象的例子,本科的时候我们宿舍和隔壁宿舍每个月都会喝次酒,单纯一直聊天不可能每个月都有话题聊,所以我们就玩一些喝酒的游戏找乐子,其中第一个游戏就是猜数字,这个游戏的玩法是:庄家从扑克牌中随机抽一张牌(扑克牌只可能是从1到13,假设庄家抽到4),其他人开始轮流猜数字,庄家负责告诉你猜的数是大了还是小了,如果把庄家卡死了(一个人猜3,另一个人5,但是没人猜4),庄家就得喝酒。
不知道我是不是解释清楚了,简化下来就是猜从1到13的一个数,刚开始我们就是随机猜,不过发现效果并不是很好,慢慢的玩了几次之后,我们开始用到了二分查找的思想:假设要猜的数字是4,因为数字1和13的中间是7,所以我们先猜7,庄家则告诉大了,这时又因为1和6中间数(向下取整)是3,所以我们这次猜3,庄家告诉我们小了,剩下的就是重复上述步骤,因为4和6的中间数是5,所以我们就猜5,这样,庄家就被卡死了。
上面这个例子就是一个二分查找实例的具体过程,它相较循环查找之所以更快捷,是因为每次判断之后它都会舍弃掉一半的元素,这样他的搜索时间就会更快,效率也会更高,当然这一切都是在数组有序的前提下。它的思想可能一说大家就都懂了,不过二分查找的实现并没有这么简单,我个人觉得它最麻烦的点在于对各种临界情况和边界的判断。如果是在电脑上实现二分查找可以通过debug不断地去调试自己的代码移以达到最优,但是手写二分查找就需要自己不断地进行练习和巩固了。
二分查找详解链接: 详解二分查找算法
代码实现 :这个是我自己写的、二分查找,我自己测试完是没有问题的,发现问题的同学可以直接评论告诉我,万分感谢!!!
package Util;
public class Search {
/**
* 查找整形数组中目标值的下标
* 二分查找法 适用用给定的数组是有序的情况,此时比循环查找法的效率要高
* @param arr 整形有序数组
* @param target 查找目标值
* @return 目标值在数组中的下标(没有查到则返回 -1)
*/
public static int binarySearch(int[] arr, int target){
int start = 0;
int end = arr.length - 1;
int middle = (start + end) >> 1;
while(middle >= start && middle <= end){
if(target > arr[middle])
start = middle + 1;
else if(target < arr[middle])
end = middle - 1;
else if(target == arr[middle])
return middle;
middle = (start + end) >> 1;
}
return -1;
}
}
2.排序
排序,同样先用最简单的语言描述就是:将一堆无规则的物品按照指定的规则进行排列。而在编程中则被进一步具体化为:将一个无序的数组按照从大到小或从小到大的顺序进行重新排列。下面所介绍的三种方法就是针对这一问题给出的三种不同的解决方案(均是从小到大排列)。在最后,我还对这三种算法的性能进行了简单的分析和比较。
2.1 冒泡排序法
算法思想描述: 如果说循环查找是最简单的查找方法,那么冒泡排序就是排序算法中的"循环查找"。下面我还是举一个实例尽可能的去解释清楚冒泡排序法的大致过程供大家参考,同样我还是会贴出一篇我参考过的文章方便大家进一步去理解各种排序算法。
假设存在一个待排序的数组arr = [3, 4, 2, 7, 9, 4, 1]。
冒泡排序法过程:
首先我们要做的就是通过一一比较之后将这7个元素中最大的元素找出来并放到数组中的最后一个位置。要想实现这一效果我们需要执行下面几个步骤:
1.比较第一个元素3和第二个元素4,因为3比4小,所以数组保持不变,当前数组arr = [3, 4, 2, 7, 9, 4, 1]
2.比较第二个元素4和第三个元素2,因为4比2大,所以两个元素交换位置,当前数组arr = [3, 2, 4, 7, 9, 4, 1]
3.按照上述规则依次往下进行比较即可,具体过程我就省略了。
当前比较的元素 | 比较之后的数组 |
---|---|
第三个元素4和第四个元素7 | arr = [3, 2, 4, 7, 9, 4, 1] |
第四个元素7和第五个元素9 | arr = [3, 2, 4, 7, 9, 4, 1] |
第五个元素9和第六个元素4 | arr = [3, 2, 4, 7, 4, 9, 1] |
第六个元素9和第七个元素1 | arr = [3, 2, 4, 7, 4, 1, 9] |
由于每次当左边的元素比右边的元素大时,我们就把大的元素从左边交换到右边,这样我们就能保证右边的元素一直是当前比较过的所有元素中最大的元素,进行一次循环之后,我们就能把最大的那个元素找出来,并放到最右边即数组的最后一个位置。
然后我们再把剩余的六个元素 [3, 2, 4, 7, 4, 1],重复执行上述步骤,就能找出原数组中的第二大元素,并放到数组中的倒数第二个位置,以此类推,就实现了对数组从小到大的排序。
我可能讲的还是比较乱,我是想用一个实例给大家描述清楚整个算法过程,这样可能会比较好理解一点,不过如果没看懂的童鞋可以参考下下面的这篇文章,讲的还是比较好的。
冒泡排序法详解链接: 三分钟彻底理解冒泡排序
代码实现 :
package Util;
public class Sort {
/**
* 对传入的整形数组进行排序(从小到大)
* 使用冒泡排序法
* @param arr 乱序的整形数组
*/
public static int bubbleSort(int[] arr){
int count = 0;
int len = arr.length;
for(int i = 0; i < len; i++){
//每循环一次,将数组中剩余的元素中最大的数字放到最后面
for(int j = 0; j < len - i - 1; j++){
//如果前一个数值比后一个数值大,则交换。
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
count++;
}
}
}
return count;
}
}
2.2 选择排序法
算法思想描述: 选择排序法是对冒泡排序法的一种改进,是利用空间换时间的一种方法,所以它的排序速度相比冒泡排序是更快的,回想一下冒泡排序,它是通过一一比较,然后交换位置从而把最大的元素移动到数组的最后一个位置。这样其实我们做了很多无谓的交换操作。但是如果我们用一个变量一直记录当前最大的元素和其下标,然后经过一次循环之后,再将其和最后一个元素进行交换呢,这样每次循环之后就只需进行一次交换操作就好了。
当待排序数组的元素数量较多时,选择排序的优势就会展现出来了。
假设存在一个待排序的数组arr = [3, 4, 2, 7, 9, 4, 1]。
选择排序法过程:
首先我们要做的就是声明两个变量保存最大值maxNumber和最大值的数组下标maxNumberOfIndex,然后循环依次进行比较,并按情况更新这两个值(可以将这两个元素的初值分别赋为啊arr[0]和3)
1.和第二个元素4进行比较,因为4比3大,所以更新maxNumber为4,maxNumberOfIndex为1。
2.和第三个元素2进行比较,因为2比4小,所以不更新maxNumber,maxNumberOfIndex。
3.按照上述规则依次往下进行比较即可,具体过程我就省略了。
当前比较的元素 | 比较之后的maxNumber | 比较之后的maxNumberOfIndex |
---|---|---|
第四个元素7 | 7 | 3 |
第五个元素9 | 9 | 4 |
第六个元素4 | 9 | 4 |
第七个元素1 | 9 | 4 |
循环之后,我们将数组中的最后一个元素和记录的最大元素进行交换,交换之后的数组arr = [3, 4, 2, 7, 1, 4, 9]
然后我们再把剩余的六个元素 [3, 2, 4, 7, 1, 4],重复执行上述步骤,就实现了对数组从小到大的排序。
同样,如果没看懂的童鞋可以参考下下面的这篇文章,讲的还是比较好的。选择排序和冒泡排序的很多细节都是大致相同的,所以把这两种方法放在一起理解应该会有更好的效果喔。
选择排序法详解链接: 三分钟彻底理解选择排序
代码实现 :我上面讲的过程是基于记录最大元素和最大元素下标,而代码实现中则是记录最小元素和最小元素下标,不过原理都是一样的,只是实现的方式不同。
package Util;
public class Sort {
/**
* 对传入的整形数组进行排序(从小到大)
* 使用选择排序法,比冒泡排序法更快
* @param arr 乱序的整形数组
*/
public static int selectSort(int[] arr){
int count = 0;
for(int i = 0; i < arr.length; i++){
int minNumber = arr[i];
int minNumberOfIndex = i;
for(int j = i + 1; j < arr.length; j++){
if(arr[j] < minNumber){
minNumber = arr[j];
minNumberOfIndex = j;
}
}
int temp = arr[i];
arr[i] = arr[minNumberOfIndex];
arr[minNumberOfIndex] = temp;
count++;
}
return count;
}
}
2.3 插入排序法
算法思想描述: 插入排序法是这三种排序算法中最快的一种算法,它使用的是一种新的思想。想象一下你有一组排列好的数字1,3,4,6,9。这时候如果传来了一个新的数字7让你插进去使其依然有序,我们可以这样做,先让其和9比较,7比9小,则7应该排在9的左边,再将其和6比较,7比6大,7应该在6的右边,这样7的位置就固定了,即在6和9中间。
插入排序法就是利用这一想法,将数组中的元素依次插进已经排好序的子序列中,这样完整循环一次数组之后,数组将全部有序。
假设存在一个待排序的数组arr = [3, 4, 2, 7, 9, 4, 1]。
插入排序法过程:
1.将数组中的第一个元素3传过来,由于此时子序列为空,所以数组不变。
2.将数组中的第二个元素4传过来,由于此时子序列为3,4比3大即应该在其右边,所以数组不变。
3.将数组中的第三个元素2传过来,因为2比4小,所以将4右移,数组arr = [3, 4, 4, 7, 9, 4, 1],又因为2比3小,所以将3右移,数组arr = [3, 3, 4, 7, 9, 4, 1],最后将2放到数组中的第一个位置,数组arr = [2, 3, 4, 7, 9, 4, 1]。
3.按照上述规则依次往下进行比较即可,具体过程我就省略了。
插入排序的思想相较于上两种较为复杂,也更难理解,不仅如此,它的代码实现也存在很多的技巧和细节,这个真的强烈建议大家多花点时间去找些资源加深一下理解,并多加练习,应该就能完全掌握了。
插入排序法详解链接: 三分钟彻底理解插入排序
代码实现 :
package Util;
public class Sort {
/**
* 对传入的整形数组进行排序(从小到大)
* 插入排序法 三种排序中最快的
* @param arr 乱序的整形数组
*/
public static void insertSort(int[] arr){
for(int i = 0; i < arr.length; i++){
int lastIndex = i - 1;
int newValue = arr[i];
while(lastIndex >=0 && newValue < arr[lastIndex]){
arr[lastIndex + 1] = arr[lastIndex];
lastIndex--;
}
//将插入的元素放到合适的位置
arr[lastIndex + 1] = newValue;
}
}
}
2.3 性能比较(感兴趣的可以看下喔)
上面对算法讲解时,同时也已经对性能进行了简单分析,总体而言插入排序所消耗的时间小于选择排序,选择排序所消耗的时间小于冒泡排序。
我是通过随机产生一个长度为10000的数组,然后统计三种排序分别所需要的时间进行比较的,除此之外我还对冒泡排序和选择排序所执行的交换操作次数进行了统计对比,下面我把自己的代码和结果贴出来,有兴趣的同学可以参考一下。
详细代码 :
package Demo_3;
import Util.Search;
import Util.Sort;
import java.util.Arrays;
public class demo1 {
public static void main(String[] args){
int length = 10000;
//对三种排序进行性能测试
//选择排序
int[] arr1 = createRandomArray(length);
long start1 = System.currentTimeMillis();
int count1 = Sort.selectSort(arr1);
long end1 = System.currentTimeMillis();
System.out.println("选择排序交换操作的次数:"+count1+"次");
System.out.println("选择排序花费时间"+(end1-start1)+"ms");
System.out.println("选择排序后的数组"+Arrays.toString(arr1));
//冒泡排序
int[] arr2 = createRandomArray(length);
long start2 = System.currentTimeMillis();
int count2 = Sort.bubbleSort(arr2);
long end2 = System.currentTimeMillis();
System.out.println("冒泡排序交换操作的次数:"+count2+"次");
System.out.println("冒泡排序花费时间"+(end2-start2)+"ms");
System.out.println("冒泡排序后的数组"+Arrays.toString(arr2));
//插入排序
int[] arr3 = createRandomArray(length);
long start3 = System.currentTimeMillis();
Sort.insertSort(arr3);
long end3 = System.currentTimeMillis();
System.out.println("插入排序花费时间"+(end3-start3)+"ms");
System.out.println("插入排序后的数组"+Arrays.toString(arr3));
}
//创建一个指定长度的随机整数数组
private static int[] createRandomArray(int length) {
int[] array = new int[length];
for(int i=0;i<array.length;i++){
array[i] = (int)(Math.random()*length);
}
return array;
}
}
执行结果 :
总结
这篇写的太累了,肝到11点了,当然这只是查找和排序最基础的东西,算是对以前知识的一个回顾吧,也为之后能够更顺畅的学习下去再次夯实一下基础,希望这次自己是真的完全掌握了记录的这些东西,多得不说啦,祝大家小年快乐,事事顺利又顺心!!!