第K大元素方法

问题:在长度为N的乱序数组中寻找第k(n>=k)大的元素。

(1)最简单直接:先排序再找

最简单直接的想法是首先进行排序。假设元素的数量不大,比如才几千个,那就可以先进行排序,比如用快排或堆排,平均时间复杂度为O(N*logN),然后取出前k个,于是总时间复杂度为O(NlogN)+O(k)=O(NlogN)。当然这种做法是浪费了不少的时间的,因为题目只要求找出第k大的元素,而不需要数据是有序的。

(2)部分元素排序:k次冒泡

当k比较小的时候,k趟排序是个比较不错的方法。我们只需要排序最大的k个元素即可,剩下那些元素不需要管。最简单明了的就是在冒泡排序中只进行k趟气泡,时间复杂度为O(N*k),适用于k相对于N很小的情况。

int findMaxK(int a[], int n, int k) {
    //进行k趟起泡
    if (k == n) k = n - 1;
    bool flag;
    for (int i = 0; i < k; i++) {
        flag = false;
        for (int j = 0; j < n - i - 1; j++) {
            if (a[j] > a[j + 1]) {
                int tmp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = tmp;
                if (!flag) flag = true;
            }
        }
        if (!flag) break;
    }
    return a[n - k];
}

(3)快排的分治法

快速排序使用了分治法的策略。它的基本思想是,选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分:在枢纽元左边的所有元素都不比它大,右边所有元素都比它大,此时枢纽元就处在它应该在的正确位置上了。
在本问题中,假设有N个数存储在数组a中。我们从a中随机找出一个元素作为枢纽元,把数组分为两部分。其中左边元素都不比枢纽元大,右边元素都不比枢纽元小。此时枢纽元所在的位置记为mid。

如果右半边(包括a[mid])的长度恰好为k,说明a[mid]就是需要的第k大元素,直接返回a[mid]。
如果右半边(包括a[mid])的长度大于k,说明要寻找的第k大元素就在右半边,往右半边寻找。
如果右半边(包括a[mid])的长度小于k,说明要寻找的第k大元素就在左半边,往左半边寻找。

比如我们选择以元素7作为基准,把数组分成了左侧较大,右侧较小的两个区域,交换结果如下:
在这里插入图片描述

包括元素7在内的较大元素有8个,但我们的K=5,显然较大元素的数目过多了。于是我们在较大元素的区域继续分治,这次以元素12为基准:
在这里插入图片描述

这样一来,包括元素12在内的较大元素有5个,正好和K相等。所以,基准元素12就是我们所求的。

这就是分治法的思想,这种方法的时间复杂度甚至优于小顶堆法,可以达到O(n)。

int divide(int a[], int low,int high) {
	//随机选一个元素作为枢纽元素
	//左边都是比枢纽元素小的,右边都是比枢纽元素大的
	srand((unsigned)time(NULL));
	int idx = (rand() % (high - low + 1)) + low;
	int tmp = a[low];
	a[low] = a[idx];
	a[idx] = tmp;
	tmp = a[low];
	while (low < high) {
		while (low<high && a[high] >= tmp) high--;
		if (low < high) {
			a[low] = a[high];
			low++;
		}
		while (low < high && a[low] <= tmp) low++;
		if (low < high) {
			a[high] = a[low];
			high--;
		}
	}
	//此时low=high,且low就是枢纽元应该在的位置编号,返回low
	a[low] = tmp;
	return low;
}
 
int findKMax(int a[],int low,int high,int k) {
	int mid = divide(a, low, high);
	//包括a[mid]的右半边长度
	int length_of_right = high - mid + 1;
	if (length_of_right == k) return a[mid];
	else if (length_of_right > k) {
		//右半边长度比k长,说明第k大的元素还在右半边,因此在右半边找
		return findKMax(a, mid + 1, high, k);
	}
	else {
		return findKMax(a, low, mid - 1, k - length_of_right);
	}
}

(4)小顶堆法

二叉堆是一种特殊的完全二叉树,它包含大顶堆和小顶堆两种形式。其中小顶堆的特点是每一个父节点都小于等于自己的两个子节点。要解决这个算法题,我们可以利用小顶堆的特性。

维护一个容量为K的小顶堆,堆中的K个节点代表着当前最大的K个元素,而堆顶显然是这K个元素中的最小值
遍历原数组,每遍历一个元素,就和堆顶比较,如果当前元素小于等于堆顶,则继续遍历;如果元素大于堆顶,则把当前元素放在堆顶位置,并调整二叉堆(下沉操作)。
遍历结束后,堆顶就是数组的最大K个元素中的最小值,也就是第K大元素

假设K=5,具体操作步骤如下:

1.把数组的前K个元素构建成堆

img

2.继续遍历数组,和堆顶比较,如果小于等于堆顶,则继续遍历;如果大于堆顶,则取代堆顶元素并调整堆。

遍历到元素2,由于2<3,所以继续遍历。
img

遍历到元素20,由于20>3,20取代堆顶位置,并调整堆。
img
img

遍历到元素24,由于24>5,24取代堆顶位置,并调整堆。
img
img

以此类推,我们一个一个遍历元素,当遍历到最后一个元素8时,小顶堆的情况如下:
img

3.此时的堆顶,就是堆中的最小元素,也就是数组中的第K大元素。

img

这个方法的时间复杂度是多少呢?

1.构建堆的时间复杂度是O(K)
2.遍历剩余数组的时间复杂度O(n-K)
3.每次调整堆的时间复杂度是O(logk)
其中2和3是嵌套关系,1和2,3是并列关系,所以总的最坏时间复杂度是O((n-k)logk + k)。当k远小于n的情况下,也可以近似地认为是O(nlogk)

这个方法的空间复杂度是多少呢?
刚才我们在详细步骤中把二叉堆单独拿出来演示,是为了便于理解。但如果允许改变原数组的话,我们可以把数组的前K个元素“原地交换”来构建成二叉堆,这样就免去了开辟额外的存储空间。因此空间复杂度是O(1)

public static int findNumberK(int[] array, int k) {
       //1.用前k个元素构建小顶堆
       buildHeap(array, k);
       //2.继续遍历数组,和堆顶比较
       for (int i = k; i < array.length; i++) {
           if(array[i] > array[0]) {
               array[0] = array[i];
               downAdjust(array, 0, k);
           }
       }
       //3.返回堆顶元素
       return array[0];
   }

   private static void buildHeap(int[] array, int length) {
       //从最后一个非叶子节点开始,依次下沉调整
       for (int i = (length - 2) / 2; i >= 0; i--) {
           downAdjust(array, i, length);
       }
   }

   /**
    * 下沉调整
    * @param array 待调整的堆
    * @param index 要下沉的节点
    * @param length 堆的有效大小
    */
   private static void downAdjust(int[] array, int index, int length) {
       //temp保存父节点的值,用于最后的赋值
       int temp = array[index];
       int childIndex = 2 * index + 1;
       while (childIndex < length) {
           //如果有右孩子,且右孩子小于左孩子的值,则定位到右孩子
           if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
               childIndex++;
           }
           //如果父节点小于任何一个孩子的值,直接跳出
           if (temp <= array[childIndex])
               break;
           //无需真正交换,单项赋值即可
           array[index] = array[childIndex];
           index = childIndex;
           childIndex = 2 * childIndex + 1;
       }
       array[index] = temp;
   }

   public static void main(String[] args) {
       int[] array = new int[] {7, 5, 15, 3, 17, 2, 20, 24, 1, 9, 12, 8};
       System.out.println(findNumberK(array, 5));
   }
可以使用快速选择算法来查找第k大的元素,其时间复杂度为O(n)。 快速选择算法的基本思路是:利用快速排序的思想,每次选取一个枢轴元素,将序列分为左右两部分,如果枢轴元素的下标为k-1,则该元素即为第k大的元素;如果枢轴元素的下标小于k-1,则在右半部分继续查找第k大的元素;如果枢轴元素的下标大于k-1,则在左半部分继续查找第k大的元素。 以下是C++代码实现: ```c++ int quickSelect(vector<int>& nums, int left, int right, int k) { int pivot = nums[left], l = left, r = right; while (l < r) { while (l < r && nums[r] <= pivot) r--; if (l < r) nums[l++] = nums[r]; while (l < r && nums[l] >= pivot) l++; if (l < r) nums[r--] = nums[l]; } nums[l] = pivot; if (l == k - 1) return pivot; else if (l < k - 1) return quickSelect(nums, l + 1, right, k); else return quickSelect(nums, left, l - 1, k); } int findKthLargest(vector<int>& nums, int k) { int n = nums.size(); return quickSelect(nums, 0, n - 1, n - k + 1); } ``` 其中,left和right表示待查找的序列的左右边界,k表示要查找的元素的下标(即第k大的元素的下标为n-k)。pivot表示枢轴元素,l和r是左右指针,用于交换元素。函数quickSelect返回第k大的元素的值。函数findKthLargest用于调用quickSelect函数,计算第k大的元素的值。 使用方法: ```c++ vector<int> nums = {3, 2, 1, 5, 6, 4}; int k = 2; int kthLargest = findKthLargest(nums, k); cout << "The " << k << "th largest element is: " << kthLargest << endl; ``` 输出结果: ``` The 2th largest element is: 5 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值