最近在学习Mark Allen Weiss这本经典著作(数据结构与算法分析-JAVA语言描述,第二版),想顺便做个笔记,一来供自己日后复习用,二来方便同样在看这本书的朋友们舒服地入门。
第一章引论部分就抛出了一个问题:取N个数中第k个最大者。
这个问题确实有很多种解法,在后面的章节中我们再去熟悉其他数据结构与算法来处理这个问题。
但现在,既然是入门嘛,我们权且把自己当成技术小白,就按照书本中的提示来,即分别利用冒泡排序和k-排序(原谅我取了这么个名字)。
先说说冒泡,但凡接触过编程的同学们对这个名词估计都不会陌生,但是让你在1分钟内写出冒泡算法,估计也不能保证写对吧,不信你试试?
对于那些工作很多年的码农朋友们,错误率甚至更高,主要是高级语言的框架封装得太好了,平常开发中不会有人去写这玩意儿,所以这里再啰嗦几句。
所谓冒泡,就是把一个的泡往上冒,想象下数组里的元素是一个个的水泡,当我们想对个数为n的一组水泡进行升序排序时,可以这样:
第0趟:冒出最大的泡,分解步骤如下。
- 第0次:拿第一个泡与第二个泡比,如果大,则交换下位置;
- 第1次:拿第二个泡与第三个泡比,如果大于,则交换下位置;
- ……
- 第n-2次:拿第n-2个泡与第n-1个泡比,如果大于,则交换下位置;
第n-1趟:冒出倒数第二大的泡,分解步骤如下。
- 第0次:拿倒数第二个泡与最后一个泡比,如果大,则交换下位置;
善于归纳的同学们应该注意到了:比较n个泡需要n-1(0到n-2)趟,第0趟要比n-1(0到n-2)次,第1趟要比n-2(0到n-3)次,第n-2趟需要比1次,每趟都少一次。
因此伪代码呼之欲出了。
// An highlighted block
趟数 from 0 to (n - 2) {
次数 from 趟数 to (n - 趟数 - 2) {
set 气泡位置 = 次数 //别问我怎么知道的,挪,看上面归纳
get 当前气泡 by 气泡位置
get 下一个气泡 by (气泡位置 + 1)
if(当前气泡 大于 下一个气泡) {
exchange 当前气泡,下一个气泡
}
}
}
转换成代码:
for(var i = 0; i <= n - 2; i++) {
for(var j = 0; j <= n - i - 2; j ++ ) {
if(array[j] > array[j + 1]) {
val temp = array[j]
array[j] = array[j + 1]
array[j + 1] = temp
}
}
}
跟以前看过的不一样?再稍微整理下~
for(var i = 0; i < n - 1; i++) {
for(var j = 0; j < n - i - 1; j ++ ) {
if(array[j] > array[j + 1]) {
val temp = array[j]
array[j] = array[j + 1]
array[j + 1] = temp
}
}
}
冒泡复习完了,我再正式回归正题。
既然求第K大的数,我们只要把数据降序再取第K-1个位置的元素不就可以了嘛。
为了第二个方法(k-排序)方便,我们将冒泡算法稍微改良下:
/**
* Bubble sort algorithm to sort array with assigned range.
* @param array is the the collection to be sorted.
* @param start is the start position of sort range.
* @param end is th end position of sort range.
*/
private fun bubbleSort(array: Array<Int>, start: Int, end: Int) {
for(i in 0 until end - start) {
for(j in 0 until end - start - i) {
if(array[start + j] < array[start + j + 1]) {
val temp = array[start + j + 1]
array[start + j + 1] = array[start + j]
array[start + j] = temp
}
}
}
}
平常我们见到的冒泡始于0终于n-1,这里我们始于start终于end(kotlin语法:until就是小于)。
所以使用冒泡求第k大元素就变得非常简单:
/**
* Get the k largest item with bubble sort, that is, bubble sort for the array first
* and then get the item at position k-1.
*/
@Test
fun kLargestWithSort() {
bubbleSort(ARR, 0, LEN - 1)
}
接下来我们看第二种方法。
/**
* Get the k largest item with bubble sort only for the previous k items inside of array, then make the remain
* items in its suitable position in the sorted sub array.
*/
@Test
fun kLargestWithKSort() {
// Bubble sort from 0 to k - 1.
bubbleSort(ARR, 0, K - 1)
// compare the remain items from k to the final with item at k-1.
for(i in K until LEN) {
// Stop when the current is lower than the k-item(position is k-1).
if(ARR[i] < ARR[K - 1]) {
continue
}
// Insert with its correct order.
insert(ARR, ARR[i])
}
}
再来看来insert定义,其实就是把某个元素插入到另一个降序后数组的合适位置,这里的“某个元素”就是k后面的所有元素。
这样代码就出来了。
/**
* Insert the assigned value to descended array.
* @param array is the array to be inserted into.
* @param insertValue is the value to be inserted.
*/
private fun insert(array: Array<Int>, insertValue: Int) {
for(i in K - 1 downTo 0) {
if(insertValue > array[i]) {
array[i + 1] = array[i]
} else {
array[i + 1] = insertValue
// No need for the next loop due to the remain items are already larger than the insert value.
break
}
}
}
我们对100000个随机数组成的数组取第8大的数,分别看两种方法耗时分别为多少。
companion object {
private const val LEN = 100000
private const val K = 8
private val random = Random()
private val ARR = Array<Int>(LEN) {position ->
random.nextInt(LEN)
}
}
执行中……
冒泡:
k-排序:
执行了多次,冒泡都是50左右,k-排序不超过10。
我们再超纲性的分析下两个方法的时间复杂度。
冒泡:
为什么bubbleSort是O(n平方)呢?
所以总的规模(很抽象一个词,等学到复杂度分析再详细讲)为:
O(n) * [O(n) * O(1)] = O(n) * O(n) = O(n^2)。
其实不知道这些字符啥玩意儿,也能分析出来:
第0趟:n - 1次;
第1趟:n - 2次;
第n - 1趟:1次。
所以总次数为 1 + 2 + … + (n - 2) + (n - 1) = (n - 1) * (1 + n - 1) / 2 = n(n - 1)/2,高阶为n平方项,即O(n^2)
k-排序
所以k-排序时间复杂度为:
O(1) + (O(n - k) * O(k))= O(kn) = O(n) //有兴趣的同学可以先看看时间复杂度计算公式。
综上,k-排序的时间复杂度比简单的冒泡少一个数量级……
这也解释了为什么一个耗时57,一个只要7,7 * 7 = 49 ~ 57。
好了,今天先记到这里吧。
项目地址:https://github.com/codersth/dsa-study
文件:KLargestTest.kt