划分的invariant
画invariant示意图,加上待处理共三段,两指针指向第一段最右、第三段最左。
从左到右划分:首元素t作pivot后,分三段:<t、>=t、待处理;初始化两指针使第一段<t为空 m=l、全是第三段待处理 i=l+1,主循环右移待处理指针i,见pp p112。记住这种写法。
|t| <t | >=t | ? |
l m i u
int t = a[l];
int m = l;
for (int i = l + 1; i <= u; i++) {
if (a[i] < t) swap(++m, i); // 往左抛
}
swap(m, l);
从右到左划分:首元素t作pivot后,分三段:待处理、<t、>=t;初始化两指针使第三段>=t为空 m=u+1、全是第一段待处理 i=u,主循环左移待处理指针i,见pp p210。不用记这种写法。
|t| ? | <t | >=t |
l i m u
int t = a[l];
int m = u + 1;
for (int i = u; i > l; i--) {
if (a[i] >= t) swap(--m, i); // 往右抛
}
swap(--m, l);
双向划分:首元素t作pivot后,分三段:<=t、待处理、>=t,两端范围都带等号;初始化两指针使第一段<=t为空 i=l、第三段>=t为空 j=u+1,loop中用do...while移动i、j指针,见pp p114。不用记这种写法。
|t| <=t | ? | >=t |
l i j u
int partition(int l, int u) {
if (l >= u) return;
swap(l, randint(l, u));
int t = a[l];
int i = l, j = u + 1;
while (true) {
do { i++; } while (i <= u && a[i] < t); // 停下时>=t
do { j--; } while (j > l && a[j] > t); // 停下时<=t
if (i > j) break;
swap(i, j);
}
swap(j, l);
return j;
}
loop中用while移动i、j指针,写法微调,记住这种写法。
// 双向划分
int partition(int l, int u) {
if (l >= u) return l;
swap(l, randint(l, u));
int t = a[l];
int i = l + 1, j = u; // 初始时多个步进
while (true) {
while (i <= u && a[i] < t) i++; // 停下时>=t
while (j > l && a[j] > t) j--; // 停下时<=t
if (i > j) break;
swap(i, j);
i++; j--; // 循环中也多个步进
}
swap(j, l);
return j;
}
三路划分:将数组分为<t、=t、>t三段
从左到右划分,遇到<t的往左抛,遇到>t的往右抛,=t的就会留在中间。
用i指向<t的待写入位置,j指向待处理位置,k指向>t的待写入位置。
|t| <t | =t | ? | >t |
l i j k u
int t = a[l];
int i = l+1, j = l+1, k = u;
while (j <= k) {
if (a[j] < t) { swap(j,i); i++; j++; }
else if (a[j] > t) { swap(j, k); k--; } // 因为从右边换过来的还待处理,不用j++
else { j++; }
}
swap(--i, l);
另:二路划分若用三路划分往两端抛的思路写,同样从左到右划分,但比标准写法繁琐:
int t = a[l];
int i = l + 1, j = u; // i指向<t的待写入部分,j指向>=t的待写入部分
while (i <= j) {
if (a[i] < t) { i++; }
else { swap(i, j); j--; }
}
swap(j, l);