Linear Time (Worst Case) Nth-Element Algorithm
看到一个有趣的算法,能在最坏时间复杂度 \(\mathcal{O}(n)\) 内求出序列第 \(k\) 小。
首先如果是期望线性的话,应该是非常简单的,就是我们直接使用快速排序的分治思想,随机选一个数作为 pivot,把比她小的放前面,比她大的放后面,于是就形成了左右两边,紧接着考虑左边的 size 是否小于 \(k\) 进行分治。
但我们需要的是确定性算法。
首先我们保证每个数是不相同的,这很好保证,只要开个 pair
,在每个数后面附上一个 index
以双关键字比较即可。
现在有一个非常有趣的想法,就是我们用一种方法来找到一个 pivot,让这个 pivot 比较居中。我们考虑将序列按照每五个切成片,这样的话如果是一个长度为 \(n\) 的序列,就会被切成 \(\left\lceil \frac{n}{5}\right\rceil\) 片,每片至多 \(5\) 个数,然后我们对这 \(5\) 个数取中位数,并将其提取出来,得到一个新的序列。这样这个新的序列的长度为 \(\left\lceil\frac{n}{5}\right\rceil\)。然后我们递归地求出这个序列的中位数(也就是该序列的第 \(\left\lfloor\frac{\left\lceil\frac{n}{5}\right\rceil}{2}\right\rfloor\) 小值,这也是一个 \(k\) 小值问题)。然后将该数作为 pivot 进行分治。
我们考虑这个 pivot 的性质。考虑到此时计算起来比较麻烦,又考虑到这是一个分治算法,我们可以在 \(n\le 100\) 的时候直接采用排序,不影响复杂度。而在 \(n\) 比较大的时候,为了方便计算,我们在计算中忽略底和顶、少数加减常数以及脚块对复杂度的影响。假设这个 pivot 的值是 \(p\),那么由于她是每个切片的中位数的中位数,也就是说,至少有 \(\frac{\frac{n}{5}}{2}=\frac{n}{10}\) 个切片的中位数比她小,\(\frac{n}{10}\) 个切片的中位数比她大。而在这些切片中,考虑到每个切片都有两个数大于中位数,两个数小于中位数,因此至少有 \(\frac{n}{5}\) 个非切片中位数的数大于和小于 pivot。也就是说,有 \(\frac{n}{10} + \frac{n}{5}=\frac{3}{10}n\) 个数大于和小于 pivot。那么分治时,不管是大于还是小于 \(k\),一定是有 \(\frac{3}{10}n\) 个数是会被删掉的。也就是说,每次递归只会留下 \(\frac{7}{10}n\) 个数。
这样复杂度就是:
\[T(n)=T\left(\frac{n}{5}\right) + T\left(\frac{7}{10}n\right) + \mathcal{O}(n)\]我们用第二数学归纳法证明 \(T(n)=\mathcal{O}(n)\)。我们假设 \(\exists c\in\mathbb{N}, \forall k<n, T(k)\le c\cdot k\)。归纳基础显然。
\[\begin{aligned} T(n)&=T\left(\frac{n}{5}\right) + T\left(\frac{7}{10}n\right) + \mathcal{O}(n)\\ &\le c\cdot\frac{n}{5} + c\cdot \frac{7}{10}n+\mathcal{O}(n)\\ &\le c\cdot\frac{9}{10}n + \mathcal{O}(n) \end{aligned}\]当 \(c\) 足够大到 \(\frac{c}{10}\) 大于等式中 \(\mathcal{O}(n)\) 的系数时,\(T(n)\le c\cdot n\)。
因此,该算法复杂度为线性。
前几天刚开始学 java
,那就拿 java
来练练手。
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class Nth {
/**
* Integers with a subscript, to make sure all numbers
* in the array are different.
*/
public static class DistinctNumber implements Comparable<DistinctNumber> {
private final int number, index;
/**
* @param _number the value of the number.
* @param _index the index of the number in the array.
*/
public DistinctNumber(int _number, int _index) {
this.number = _number;
this.index = _index;
}
/**
* @return the value of the number.
*/
public int valueOf() {
return number;
}
/**
* Compare {@code this} and {@code b}. If the value of the two are the
* same, compare the index.
*
* @param b the object to be compared.
* @return an integer, if {@code x < y} then {@code x.compareTo(y)
* < 0}, if {@code x > y} then {@code x.compare(y) > 0}, if {@code
* x = y} then {@code x.compare(y) = 0}.
*/
@Override
public int compareTo(DistinctNumber b) {
if (this.number != b.number) {
return this.number - b.number;
} else {
return this.index - b.index;
}
}
/**
* Convert a {@code int[]} to {@code DistinctNumber[]}.
*
* @param intArray the {@code int} array want to be converted.
* @param distinctArray the {@code DistinctNumber} array want
* to be saved into.
* @return the array after converting.
*/
public static DistinctNumber[] intArrayToDistinctArray(int[] intArray, DistinctNumber[] distinctArray) {
for (int i = 0; i < intArray.length; ++i) {
distinctArray[i] = new DistinctNumber(intArray[i], i);
}
return distinctArray;
}
/**
* Convert a {@code int[]} to {@code DistinctNumber[]}.
*
* @param intArray the {@code int} array want to be converted.
* @return the array after converting.
*/
public static DistinctNumber[] intArrayToDistinctArray(int[] intArray) {
DistinctNumber[] distinctArray = new DistinctNumber[intArray.length];
return intArrayToDistinctArray(intArray, distinctArray);
}
/**
* Convert a {@code DistinctNumber[]} to {@code int[]} .
*
* @param distinctArray the {@code DistinctNumber} array want
* to be converted.
* @param intArray the {@code int} array want to be saved into.
* @return the array after converting.
*/
public static int[] distinctArrayToIntArray(DistinctNumber[] distinctArray, int[] intArray) {
for (int i = 0; i < distinctArray.length; ++i) {
intArray[i] = distinctArray[i].valueOf();
}
return intArray;
}
/**
* Convert a {@code DistinctNumber[]} to {@code int[]} .
*
* @param distinctArray the {@code DistinctNumber} array want
* to be converted.
* @return the array after converting.
*/
public static int[] distinctArrayToIntArray(DistinctNumber[] distinctArray) {
int[] intArray = new int[distinctArray.length];
return distinctArrayToIntArray(distinctArray, intArray);
}
}
/**
* Use Hoare partition method to move all the numbers
* in the array smaller than {@code array[pivot]} to its front,
* move all the numbers larger than it to its back, and
* return the final position of the number.
*
* @param array a non-empty array wants to be rearranged.
* @param pivot an integer from {@code 0} to
* {@code array.length - 1}, which represents
* the position of the pivot.
* @return an integer from {@code 0} to {@code array.length - 1},
* which represents the position of the pivot after rearranged.
*/
public static int hoarePartition(DistinctNumber[] array, int pivot) {
int left = 0, right = array.length - 1;
while (left < right) {
/*
Find two positions left and right which
satisfies that array[left] > array[pivot]
> array[right] and left <= pivot <= right.
*/
while (left < pivot &&
array[left].compareTo(array[pivot]) <= 0) {
left++;
}
while (right > pivot &&
array[right].compareTo(array[pivot]) >= 0) {
right--;
}
/*
If pivot equals to left or right, which means
the position of the pivot changes. So we have
to change the position of the pivot before
swapping left and right.
*/
if (pivot == left) {
pivot = right;
} else if (pivot == right) {
pivot = left;
}
/*
Swap array[left] and array[right].
*/
DistinctNumber temp = array[left];
array[left] = array[right];
array[right] = temp;
}
return pivot;
}
/**
* Divide an array into every five integer a piece, extract
* the median of each copy to the front end of the entire
* array, copy the front and return it.
*
* @param array a non-empty {@code DistinctNumber} array want
* to be rearranged.
* @return a non-empty array, contains the median of each
* slice.
*/
public static DistinctNumber[] divideIntoFive(DistinctNumber[] array) {
List<DistinctNumber> mediumList = new LinkedList<>(); // Median of each slice
List<DistinctNumber> remainList = new LinkedList<>(); // Numbers other than the median
for (int beginSlice = 0; beginSlice < array.length; beginSlice += 5) {
// Extract a slice.
int endSlice = Integer.min(beginSlice + 5, array.length);
DistinctNumber[] slice = Arrays.copyOfRange(array, beginSlice, endSlice);
// Sort the slice and assign each number to each linked list
Arrays.sort(slice);
for (int i = 0; i < slice.length; ++i) {
if (i == slice.length / 2) {
mediumList.add(slice[i]);
} else {
remainList.add(slice[i]);
}
}
}
// Copy mediumList and remainList to the array.
int indexOfChangedArray = 0;
for (DistinctNumber medium : mediumList) {
array[indexOfChangedArray] = medium;
indexOfChangedArray++;
}
for (DistinctNumber remain : remainList) {
array[indexOfChangedArray] = remain;
indexOfChangedArray++;
}
// Convert mediumList to array and return.
DistinctNumber[] mediumArray = new DistinctNumber[mediumList.size()];
mediumList.toArray(mediumArray);
return mediumArray;
}
/**
* Give an {@code DistinctNumber} array and a number n,
* rearrange the array to make that for all {@code i < n},
* {@code array[i] <= array[n]}; for all {@code i > n},
* {@code array[i] >= array[n]}.
*
* @param array a non-empty {@code DistinctNumber} array want
* to be rearranged.
* @param n an integer from {@code 0} to {@code n - 1}.
*/
public static void nthElement(DistinctNumber[] array, int n) {
if (n >= array.length) {
throw new ArrayIndexOutOfBoundsException("n cannot " +
"greater than the size of array");
}
// Recursion bounds
if (array.length <= 10) {
Arrays.sort(array);
return;
}
// Get the pivot
DistinctNumber[] mediumOfSlice = divideIntoFive(array);
nthElement(mediumOfSlice, mediumOfSlice.length / 2);
int pivot = mediumOfSlice.length / 2;
System.arraycopy(mediumOfSlice, 0, array, 0, mediumOfSlice.length);
pivot = hoarePartition(array, pivot);
// Divide and conquer by pivot
if (n < pivot) {
DistinctNumber[] leftSide = Arrays.copyOfRange(array, 0, pivot);
nthElement(leftSide, n);
System.arraycopy(array, 0, leftSide, 0, leftSide.length);
} else if (n > pivot) {
DistinctNumber[] rightSide = Arrays.copyOfRange(array, pivot + 1, array.length);
nthElement(rightSide, n - pivot - 1);
System.arraycopy(array, pivot + 1, rightSide, 0, rightSide.length);
}
}
/**
* Give an array and a number n, rearrange the array to make
* that for all {@code i < n}, {@code array[i] <= array[n]};
* for all {@code i > n}, {@code array[i] >= array[n]}.
*
* @param array a non-empty array want to be rearranged.
* @param n an integer from {@code 0} to {@code n - 1}.
*/
public static void nthElement(int[] array, int n) {
DistinctNumber[] distinctArray = DistinctNumber.intArrayToDistinctArray(array);
nthElement(distinctArray, n);
}
}