【一起学数据结构与算法分析】第一篇:取第K大数的两种方法比较

最近在学习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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

章代沫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值