抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

看到一个有趣的算法,能在最坏时间复杂度 O(n)\mathcal{O}(n) 内求出序列第 kk 小。

首先如果是期望线性的话,应该是非常简单的,就是我们直接使用快速排序的分治思想,随机选一个数作为 pivot,把比她小的放前面,比她大的放后面,于是就形成了左右两边,紧接着考虑左边的 size 是否小于 kk 进行分治。

但我们需要的是确定性算法。

首先我们保证每个数是不相同的,这很好保证,只要开个 pair,在每个数后面附上一个 index 以双关键字比较即可。

现在有一个非常有趣的想法,就是我们用一种方法来找到一个 pivot,让这个 pivot 比较居中。我们考虑将序列按照每五个切成片,这样的话如果是一个长度为 nn 的序列,就会被切成 n5\left\lceil \frac{n}{5}\right\rceil 片,每片至多 55 个数,然后我们对这 55 个数取中位数,并将其提取出来,得到一个新的序列。这样这个新的序列的长度为 n5\left\lceil\frac{n}{5}\right\rceil。然后我们递归地求出这个序列的中位数(也就是该序列的第 n52\left\lfloor\frac{\left\lceil\frac{n}{5}\right\rceil}{2}\right\rfloor 小值,这也是一个 k 小值问题)。然后将该数作为 pivot 进行分治。

我们考虑这个 pivot 的性质。考虑到此时计算起来比较麻烦,又考虑到这是一个分治算法,我们可以在 n100n\le 100 的时候直接采用排序,不影响复杂度。而在 nn 比较大的时候,为了方便计算,我们在计算中忽略底和顶、少数加减常数以及脚块对复杂度的影响。假设这个 pivot 的值是 pp,那么由于她是每个切片的中位数的中位数,也就是说,至少有 n52=n10\frac{\frac{n}{5}}{2}=\frac{n}{10} 个切片的中位数比她小,n10\frac{n}{10} 个切片的中位数比她大。而在这些切片中,考虑到每个切片都有两个数大于中位数,两个数小于中位数,因此至少有 n5\frac{n}{5} 个非切片中位数的数大于和小于 pivot。也就是说,有 n10+n5=310n\frac{n}{10} + \frac{n}{5}=\frac{3}{10}n 个数大于和小于 pivot。那么分治时,不管是大于还是小于 kk,一定是有 310n\frac{3}{10}n 个数是会被删掉的。也就是说,每次递归只会留下 710n\frac{7}{10}n 个数。

这样复杂度就是:

T(n)=T(n5)+T(710n)+O(n)T(n)=T\left(\frac{n}{5}\right) + T\left(\frac{7}{10}n\right) + \mathcal{O}(n)

我们用第二数学归纳法证明 T(n)=O(n)T(n)=\mathcal{O}(n)。我们假设 cN,k<n,T(k)ck\exist c\in\N, \forall k<n, T(k)\le c\cdot k。归纳基础显然。

T(n)=T(n5)+T(710n)+O(n)cn5+c710n+O(n)c910n+O(n)\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}

cc 足够大到 c10\frac{c}{10} 大于等式中 O(n)\mathcal{O}(n) 的系数时,T(n)cnT(n)\le c\cdot n

因此,该算法复杂度为线性。

前几天刚开始学 java,那就拿 java 来练练手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
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);
}
}

评论