20172313 2018-2019-1 《程序设计与数据结构》第五周学习总结
20172313 2018-2019-1 《程序设计与数据结构》第五周学习总结
教材学习内容总结
- 查找
- 查找是这样一个过程,即在某个项目组中寻找某一指定目标元素,或者确定该指定目标并不存在。
- 高效的查找会使该过程所做的比较操作次数最小化。
- 通过将方法声明成泛型,我们可以将所有的排序和查找方法位于这个类中。但是,对于这种方式定义的Searching类,要求我们在使用查找或排序方法时必须实例化。这对只含有方法而无其他内容的类来说是一件糟糕的事情,一种更好的解决方案是把所有方法声明为静态或泛型。
public classs Searching<T extends Comparable<T>>
- 要创建一个泛型方法,只需在方法头的返回类型前插入一个泛型声明即可。
public static <T extends Comparable<T>> Boolean
LinearSearch (T[] data, int min, int max, Ttarget)
- 线性查找法:从该列表头开始依次比较每一个值,直到找到该目标元素。
- 二分查找法
- 二分查找要求查找池中的项目组都是已排序的。
- 二分查找的每次比较都会删除一半的可行候选项。
- 查找算法的比较
- 线性查找算法具有限行时间复杂度O(n)。因为试一次每回查找一个元素,所以复杂度是线性的————直接与待查找元素数目成比例。
- 二分查找具有一个对数算法且具有时间复杂度O(l0g2n)。二分查找的复杂度是对数集的,这使得它对于大型查找池非常有效率。
- 如果说对数查找比线性查找更有效率,那么为什么我们还要使用线性查找?第一,线性查找一般比二分查找要简单,因此我们编程和调试起来更容易。第二,线性查找无需花费额外成本来排序该查找列表。在将查找池保持为排序状态和提高查找效率的努力之间存在着权衡关系。
- 排序
- 排序是这样一个过程,即基于某一标准,将某一组项目按照某个规定顺序排列。
- 排序算法通常分为两类:顺序排序: 它通常使用一对嵌套循环对n个元素排序,需要大约n2次比较;对数排序: 它对n个元素进行排序通常需要大约nlog2n次比较。
- 选择排序法:通过反复地将某特定值放到它在列表中的最终已排序位置从而完成对某一列表值的排序:(1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置(2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。(3)重复第二步,直到所有元素均排序完毕。。
- 插入排序:通过反复地将某一特定值放到它在列表中的最终已排序位置从而完成对某一列表值的排序:(1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。(2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
- 冒泡排序法:冒泡排序法通过重复地比较相邻元素且在必要时将他们互换,从而完成对某个列表的排序。(1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。(2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。(3)针对所有的元素重复以上的步骤,除了最后一个。(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
- 快速排序法:通过将列表分析,然后对着两个分区进行递归式排序,从而完成对某个列表的排序。(1)从数列中挑出一个元素,称为 “基准”(pivot)(2)重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。(3) 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
- 归并排序法:通过将列表递归式分成两半直至每一子列表都含有一个元素,然后将这些子列表归并到一个排序顺序中,从而完成对列表的排序。(1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置(4)重复步骤3直到某一指针达到序列尾(5)将另一序列剩下的所有元素直接复制到合并序列尾
- 基数排序法:是基于队列处理,使用排列密钥而不是直接地进行比较元素,来实现元素排序。是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。 简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。
- 各种排序算法效率比较
教材学习中的问题和解决过程
- 问题一:在我的理解中,冒泡排序就是比较相邻位置的元素,根据大小将它们互换。而插入排序把特定值放到已排序的位置也需要相邻元素的比较,那么它们两者的实现的区别到底在哪?
- 问题一解决方法:
算法名称 | 最差时间复杂度 | 平均时间复杂度 | 最优时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(N^2) | O(N^2) | O(N) | O(1) | 稳定 |
插入排序 | O(N^2) | O(N^2) | O(N) | O(1) | 稳定 |
两者在数据上简直一毛一样,只考虑复杂度的话完全可以互相替代。但深究的话,还是能找出许多不同:
打个比方:这是我们今天的主角小明,他机智勇敢热爱学习乐于助人。有一天他上体育课,排的是3号位置,老师说:同7学们请用冒泡排序的方法排好队。小明觉得自己比2号的小红高,所以互换位置,成为了2号。然后他觉得比1号小刚高,所以又互换位置排到了1号。老师说:小明,滚到最后去。最终他成了100号,这就是插入排序。有一天他上体育课,排的是3号位置,老师说:同学们请用冒泡排序的方法排好队。小明觉得自己比2号的小红高,所以互换位置,成为了2号。然后他觉得比1号小刚高,所以又互换位置排到了1号。老师说:小明,滚到最后去。最终他成了100号,这就是插入排序。
插入排序:
将无序的元素插入到有序的元素序列中,插入后仍然有序
int min;
T temp;
for(int index = 0; index < data.length-1;index++)
{
min = index;
for(int scan = index + 1; scan < data.lenth; scan ++)
if(data[scan].compareTo(data[min])<0)
min = scan;
/*8swap the values*/
temp = data[min];
data[min] = data[index];
data[index] = temp;
}
/*
举例:从小到大排列[2,1,4,3]
第一趟排序:[1,2,4,3] 交换次数:1 比较次数:1
第二趟排序:[1,2,4,3] 交换次数:0 比较次数:1
第三趟排序:[1,2,3,4] 交换次数:1 比较次数:2
从小到大排列[4,3,2,1]
第一趟排序:[3,4,2,1] 交换次数:1 比较次数:1
第二趟排序:[2,3,4,1] 交换次数:2 比较次数:2
第三趟排序:[1,2,3,4] 交换次数:3 比较次数:3
*/
冒泡排序:
比较相邻元素,直到序列变为有序为止
int position, scan;
T temp;
for(position = data.length - 1; position >= 0; position--){
int position, scan;
T temp;
for(position = data.length - 1;position >= 0; position--){
for(scan = 0;scan <= position - 1;scan++){
if(data[scan].compareTo(data[scan+1]) > 0)
swap(data,scan, scan + 1);
}
}
}
/*
举例:从小到大排列[2,1,4,3]
第一趟排序:[1,2,4,3] 交换次数:1 比较次数:3
第二趟排序:[1,2,4,3] 交换次数:0 比较次数:2
第三趟排序:[1,2,3,4] 交换次数:1 比较次数:1
从小到大排列[4,3,2,1]
第一趟排序:[3,2,1,4] 交换次数:3 比较次数:3
第二趟排序:[2,1,3,4] 交换次数:2 比较次数:2
第三趟排序:[1,2,3,4] 交换次数:1 比较次数:1
*/
- 问题二:在快速排序法中,如果说可以将随意一个数作为分区元素的话,那么如何保证分区元素前后元素个数相同呢?对快速排序法不是很了解。
- 问题三解决方案:我对于快速排序法理解有误。假设我们现在对“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面前。
现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:
6 1 2 5 9 3 4 7 10 8
到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:
6 1 2 5 4 3 9 7 10 8
第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下:
3 1 2 5 4 6 9 7 10 8
到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。
OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。
- 问题三:在问题二中提到因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要,对这句话不是特别理解。
- 问题三解决方案:如果选取最左边的数arr[left]作为基准数,那么先从右边开始可保证i,j在相遇时,相遇数是小于基准数的,交换之后temp所在位置的左边都小于temp。但先从左边开始,相遇数是大于基准数的,无法满足temp左边的数都小于它。所以进行扫描,要从基准数的对面开始,又或者选取最中间的数作为基准数就不会出现类似问题。
- 问题四:在学习基数排序法的时候,了解到基数排序是基于队列处理的,并且要基于排序关键字,但基数排序法是如何实现的?基数和排序关键字又是如何选取的?
- 问题四解决方案:我觉得用理论来解释是空泛的,举个例子能更好的帮助理解。
例如要对大小为[1..1000]范围内的n个整数A[1..n]排序
首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储 (10..20]的整数,……集合B[i]存储( (i-1)10, i10]的整数,i = 1,2,..100。总共有 100个桶。
然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。
最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是
O(n + m * n/m*log(n/m)) = O(n + nlogn – nlogm)
从上式看出,当m接近n的时候,桶排序复杂度接近O(n)
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的 ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。
前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:
1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。
2)其次待排序的元素都要在一定的范围内等等。
代码调试中的问题和解决过程
- 问题1:在做编程项目pp9_3的时候,不知道怎么计算程序的执行时间。
- 问题一解决方案:
一般输出日期时间经常会用到Date这个类:
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
System.out.println(df.format(new Date()));// new Date()为获取当前系统时间
(1)以毫秒为单位计算
static long currentTimeMillis() , 该方法返回值是从1970年1月1日凌晨到此时刻的毫秒数
long startTime=System.currentTimeMillis(); //获取开始时间
doSomeThing(); //测试的代码段
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间: "+(end-start)+"ms");
(2)以纳秒为单位计算
long startTime=System.nanoTime(); //获取开始时间
doSomeThing(); //测试的代码段
long endTime=System.nanoTime(); //获取结束时间
System.out.println("程序运行时间: "+(end-start)+"ns");
代码托管
上周考试错题总结
这周没有错题哦~
结对及互评
- 博客中值得学习的或问题:
- 排版精美,对教材的总结细致,善于发现问题,对于问题研究得很细致,解答也很周全。
- 代码中值得学习的或问题:
- 代码写的很规范,思路很清晰,继续加油!
点评过的同学博客和代码
其他(感悟、思考等,可选)
国庆假期过去了,这周的学习效率还算可以,完成了学习任务,达到了自己预期的目标,但在看了其他同学的博客后,还是感到自己的学习时间还是远远不够,相比之下,自己的学习效率还是较低的,希望能够在以后的学习中继续进步!
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
第一周 | 200/200 | 1/1 | 5/20 | |
第二周 | 981/1181 | 1/2 | 15/20 | |
第三周 | 1694/2875 | 1/3 | 15/35 | |
第四周 | 3129/6004 | 1/4 | 15/50 | |
第五周 | 1294/7298 | 1/5 | 15/65 |
-
计划学习时间:15小时
-
实际学习时间:15小时