问题
给定一个无重复元素的无序数组,求其第m小的元素。
如:{1,5,3,6,2,8,5} 求其第3小的元素
答案:3
分析
- 直观可以考虑通过对数组进行排序求解,而基于排序算法的时间复杂度下界为 O ( n l o g n ) O(nlogn) O(nlogn),即通过排序算法求解的时间复杂度不会低于 O ( n l o g n ) O(nlogn) O(nlogn)。
- 借助快速排序思想,每次遍历作为主元素的所在的分割位置确定后固定,当前子数组排序完成后,设主元素的索引为 n n n,则 n n n即为该数组中第 n + 1 n+1 n+1小的元素,故此,求数组中第 m m m小的元素,只需在排序中的某一轮中确定下标为 m − 1 m-1 m−1处位置的主元素即可。
- 不同于快速排序的过程,寻找第m小的元素目的在于单个元素的查询,不在意所有元素的排序,故此,当轮排序完成后
- 若主元素索引小于 m − 1 m-1 m−1,则表示所查询元素位于主元素的右侧,只需对右侧子数组再进行数组划分,左侧子数组不在关注。
- 若主元素索引大于 m − 1 m-1 m−1,则表示所查询元素位于主元素的左侧,只需对左侧子数组再进行数组划分,右侧子数组不再关注。
- 若主元素等于 m − 1 m-1 m−1,则表示当前主元素即为所查询元素,查询结束。
- 该算法能够实现主要借助于快速排序中,当每一轮的排序完成,主元素位置确定后,其索引将不会在之后的子数组排序中被改变,即每一轮确定的主元素的位置与其在整体排序完成后数组中的位置保持一致的特点。
- 该算法的时间复杂度 T ( n ) T(n) T(n)满足: O ( n ) ≤ T ( n ) ≤ O ( n 2 ) O(n) \le T(n) \le O(n^2) O(n)≤T(n)≤O(n2)
- 该算法的空间复杂度为 O ( n ) O(n) O(n),若不保证原数组的次序,则空间复杂度为 O ( 1 ) O(1) O(1).
代码
-
c++
#include <exception> #include <vector> #include <ctime> using std::vector; using std::exception; using std::rand; class MMin { private: vector<int>* parr = nullptr; // 重新分配被查询数组,避免改变原数组,成员替代递归传参 int m; int mmin; // 存放查询到的结果 public: int m_min(const vector<int>& arr, int m) { // 对外接口 if (arr.size() == 0) throw exception("arr cannot be empty!"); if (m < 1 || m > arr.size()) throw exception("m cannot be out of range!"); this->parr = new vector<int>(arr); // 复制数组用于操作 this->m = m; this->_m_min(0, this->parr->size()); delete this->parr; // 释放临时数组 this->parr = nullptr; return this->mmin; } private: void _m_min(int left, int right) { // 内部递归实现 // 一个元素时终止递归 if (left + 1 == right) { // 一个元素时直接判断该元素索引是否符合条件 if (this->m - 1 == left) this->mmin = this->parr->at(left); return; } // 随机选取主元素,交换至尾元素 std::srand(std::time(nullptr)); int pos = rand() % (right - left) + left; int main_element = this->parr->at(pos); (*this->parr)[pos] = (*this->parr)[right - 1]; (*this->parr)[right - 1] = main_element; // 数组划分,遍历除主元素外的其余元素 int i = left - 1, j = left, temp = 0; for (; j < right - 1; j++) { if (this->parr->at(j) < main_element) { temp = (*this->parr)[i + 1]; (*this->parr)[i + 1] = (*this->parr)[j]; (*this->parr)[j] = temp; i++; } } // 交换主元素至中间,完成数组划分 temp = main_element; (*this->parr)[right - 1] = (*this->parr)[i + 1]; (*this->parr)[i + 1] = temp; i++; // i指向主元素 // 当主元素索引恰好满足条件,查询结束 if (this->m - 1 == i) this->mmin = this->parr->at(i); // 当主元素索引大于m-1,则对左侧子数组进行排序查询 else if (this->m - 1 < i) this->_m_min(left, i); // 当主元素索引小于m-1,则对右侧子数组进行排序查询 else this->_m_min(i + 1, right); } };
-
java
import java.lang.RuntimeException; import java.util.Random; public class MMin { // 存放被查询数组的拷贝,避免改变原数组,成员替代递归传参 private int[] arr = null; private int m; private int mmin; // 存放查询到的结果 public int m_min(final int[] arr, int m) { // 对外接口 if (arr.length == 0) throw new RuntimeException("arr cannot be empty!"); if (m < 1 || m > arr.length) throw new RuntimeException("m cannot be out of range!"); this.arr = new int[arr.length]; // 复制数组用于操作 for (int i = 0; i < arr.length; i++) this.arr[i] = arr[i]; this.m = m; this._m_min(0, this.arr.length); this.arr = null; // 拷贝数组置空 return this.mmin; } private void _m_min(int left, int right) { // 内部递归实现 // 一个元素时终止递归 if (left + 1 == right) { // 一个元素时直接判断该元素索引是否符合条件 if (this.m - 1 == left) this.mmin = this.arr[left]; return; } // 随机选取主元素,交换至尾元素 Random rdm = new Random(); int pos = rdm.nextInt(right - left) + left; int main_element = this.arr[pos]; this.arr[pos] = this.arr[right - 1]; this.arr[right - 1] = main_element; // 数组划分,遍历除主元素外的其余元素 int i = left - 1, j = left, temp = 0; for (; j < right - 1; j++) { if (this.arr[j] < main_element) { temp = this.arr[i + 1]; this.arr[i + 1] = this.arr[j]; this.arr[j] = temp; i++; } } // 交换主元素至中间,完成数组划分 temp = main_element; this.arr[right - 1] = this.arr[i + 1]; this.arr[i + 1] = temp; i++; // i指向主元素 // 当主元素索引恰好满足条件,查询结束 if (this.m - 1 == i) this.mmin = this.arr[i]; // 当主元素索引大于m-1,则对左侧子数组进行排序查询 else if (this.m - 1 < i) this._m_min(left, i); // 当主元素索引小于m-1,则对右侧子数组进行排序查询 else this._m_min(i + 1, right); } };
-
python
from random import randint class MMin: def __init__(self): self.__arr = [] # 存放查找数组的拷贝,排序将不改变原数组 self.__mmin = None # 存放第m小的元素 self.__m = None # 存放m,减少递归传参 def m_min(self, arr: list, m: int) -> int: if len(arr) == 0: # 数组判空 raise Exception("arr cannot be empty.") if m < 1 or m > len(arr): # 判断m是否有效 raise Exception("m cannot be out of range.") self.__arr = arr[:] self.__m = m self.__m_min(0, len(arr)) self.__arr.clear() return self.__mmin def __m_min(self, left: int, right: int) -> None: # 一个元素时递归终止 if left + 1 == right: # 当仅剩一个元素时直接判断该元素索引是否满足条件 if self.__m - 1 == left: self.__mmin = self.__arr[left] return None # 随机选取主元素,交换至尾元素 pos = randint(left, right-1) main_element = self.__arr[pos] self.__arr[pos] = self.__arr[right - 1] self.__arr[right - 1] = main_element # 遍历除主元素外的其余元素,数组划分 i = left - 1 j = left while j < right - 1: if self.__arr[j] < main_element: temp = self.__arr[j] self.__arr[j] = self.__arr[i+1] self.__arr[i+1] = temp i += 1 j += 1 # 交换尾部主元素至中间作为分割,完成数组划分 temp = main_element self.__arr[right-1] = self.__arr[i+1] self.__arr[i+1] = temp i += 1 # 使i指向主元素 # 当主元素索引正好满足条件时,直接返回 if self.__m - 1 == i: self.__mmin = main_element # 当主元素索引大于m-1时,排序查找左侧子数组 elif self.__m - 1 < i: self.__m_min(left, i) # 当主元素索引小于m-1时,排序查找右侧子数组 else : self.__m_min(i+1,right)