看标题。你可能会疑惑:咦?你这家伙。怎么不解说完整的快排,仅仅讲一部分快排……-。- 哎,冤枉。

“部分快排”是算法的名字。实际上本文相当具体呢。本文差点儿与普通快排无异。看懂了本文,你对普通的快排也会有更深的认识了。

  高速排序算法(qsort)的原理我们大都应该了解。本文介绍的是部分高速排序算法

事实上其算法本质是一样的,仅仅只是限定了排序的左右区间。也就是仅仅对一个数字序列的一部分进行排序。故称为“部分高速排序算法”。简称:pqsort

  Redis项目中的pqsort.c 文件实现了pqsort()函数。其源代码见本文最后一节 pqsort.c源代码 。 另外补充一句:长文慎入 :-)  


导读

外部资料

维基百科

  快排基本流程不了解的童鞋。请移步 高速排序wiki

论文

  实际上pqsort.c的快排流程是改编自一个经典实现,该实现被很多的实现所使用。

请參考Bentley & McIlroy所著论文“Engineering a Sort Function”

源代码结构

主要函数 pqsort()
静态函数 _pqsort()、swapfuc()、med3()
宏函数 min()、swap()、swapcode()、vecswap()、SWAPINIT()

  整体来说,pqsort.c文件对外仅仅提供了一个函数—— pqsort() ,但它的算法逻辑事实上是由_pqsort()实现的,其它的静态(static)函数和宏也都是为了该函数服务的。

  接下来的介绍中。我会简介宏函数和几个静态函数。把重点放在静态函数_pqsort()上。它才是整个算法的核心部分。

pqsort()与qsort()

  C标准库中有一个快排的函数qsort(),它与本文介绍的pqsort()所提供的编程接口极为类似,请看两者声明:

void qsort (void *a, size_t n, size_t es, int (*cmp)(const void *, const void *));
void pqsort(void *a, size_t n, size_t es, int (*cmp)(const void *, const void *),
size_t lrange, size_t rrange);

參数解读

參数 说明
a 待排序数组的首地址
n 待排序元素的个数
es element size:每一个元素的字节大小
cmp 回调函数。定义了比較的规则,直接影响排序结果是递增排序或递减排序,并支持非标准类型的排序
lrange 待排序的左边界
rrange 待排序的右边界

pqsort()与_pqsort()

pqsort()源代码

void
pqsort(void *a, size_t n, size_t es,
int (*cmp) (const void *, const void *), size_t lrange, size_t rrange)
{
_pqsort(a,n,es,cmp,((unsigned char*)a)+(lrange*es),
((unsigned char*)a)+((rrange+1)*es)-1);
}

  能够看出我们的qpsort()事实上是在调用_pqsort()来完毕排序功能的。

这两个函数非常像,区别在于參数上。

看一下两者的函数原型:

void
pqsort (void *a, size_t n, size_t es, int (*cmp)(const void *, const void *),
size_t lrange, size_t rrange);
static void
_pqsort(void *a, size_t n, size_t es, int (*cmp)(const void *, const void *),
void *lrange, void *rrange)

差异的关键在于:

  • pqsort() 的參数中的左右边界值,其含义值下标
  • _pqsort()的參数中的左右边界值,其含义是指针

这样pqsort()源代码就不足为奇了。所以我前面说该文件的核心部分是_pqsort()

预备知识

  看一下除了_pqsort()之外的源代码部分。这些都是_pqsort()函数实现的辅助。

med3

static inline char *
med3(char *a, char *b, char *c,
int (*cmp) (const void *, const void *))
{ return cmp(a, b) < 0 ? (cmp(b, c) < 0 ? b : (cmp(a, c) < 0 ? c : a ))
:(cmp(b, c) > 0 ? b : (cmp(a, c) < 0 ? a : c ));
}

  依据回调函数cmp指定的比較规则。则求出变量a,b,c中处于中间大小的变量。

换句话说:就是在求 中位数

min

#define min(a, b)   (a) < (b) ? a : b

  这是个简单的宏。看一眼就呵呵即可了。

SWAPINIT

#define SWAPINIT(a, es) swaptype = ((char *)a - (char *)0) % sizeof(long) || \
es % sizeof(long) ? 2 : es == sizeof(long)? 0 : 1;

  该宏的目的在于,给swaptype赋值,它有例如以下几种取值:

swaptype 说明
0 数组a中每一个元素的大小是sizeof(long)
1 数组a中每一个元素的大小是sizeof(long)的倍数。但不等于sizeof(long)
2 数组a中每一个元素的大小不是sizeof(long)的倍数
其它 数组a的首地址不是sizeof(long)的倍数,即不是总线字节对齐

  swaptype等于0、1、2的时候,数组a的首地址都是sizeof(long)字节对齐的。

题外话:

  我们常说8字节对齐,指的是64位机器中要满足8字节对齐(首地址是8的倍数),则数据的读取效率会更高。而32位系统应满足的是4字节对齐。

具体大小是和机器字长相关的,机器字长指的是计算机一次能读取的二进制位数。

一般机器字长和long类型的大小同样,所以能够说要满足sizeof(long)字节对齐。


以下首先介绍的是几个与交换操作相关的函数(或宏),这里我假定A → B表示A函数会调用B函数(宏)。

  我们从右向左解读

swapcode

  这是个宏函数 。其功能是将以parmiparmj为首地址的n个字节进行交换。

#define swapcode(TYPE, parmi, parmj, n) {       \
  • 形參TYPE 就是指的类型。阅读后面代码。可知其实參charlong这两种。

  • 形參n 指定的是待交换字节数。

请同意我在这里。使用了术语:形參实參。尽管可能不搭,但目的是便于读者理解。

    size_t i = (n) / sizeof (TYPE);         \
TYPE *pi = (TYPE *)(void *)(parmi); \
TYPE *pj = (TYPE *)(void *)(parmj); \

  i就是指定类型(char或long)的元素的个数。然后将參数parmiparmj转换成指定的类型的指针pipj

    do {                        \
TYPE t = *pi; \
*pi++ = *pj; \ //等价于*pi = *pj; pi++;
*pj++ = t; \
} while (--i > 0); \
} //end of #define

  一个do-while循环,内部执行了交换操作。

swapfunc

static inline void
swapfunc(char *a, char *b, size_t n, int swaptype)
{ if (swaptype <= 1)
swapcode(long, a, b, n)
else
swapcode(char, a, b, n)
}

  简单的if条件语句。假设swaptype <= 1(swaptype为0或1。即元素类型为sizeof(long)的倍数)则按long类型的大小来进行交换。否则就按char类型的大小来进行交换。

这样做的目的主要是提高交互操作的效率。

swap

#define swap(a, b)                      \
if (swaptype == 0) { \
long t = *(long *)(void *)(a); \
*(long *)(void *)(a) = *(long *)(void *)(b); \
*(long *)(void *)(b) = t; \
} else \
swapfunc(a, b, es, swaptype)

  前面已经说过了,swaptype为0的时候,表示数组元素的大小等于long类型的大小。所以这里进行了这种交互操作。

vecswap

#define vecswap(a, b, n) if ((n) > 0) swapfunc((a), (b), (size_t)(n), swaptype)

  该宏和swap(a, b)事实上非常像。都是在调用swapfunc来完毕交互操作。

但而二者的不同之处是:vecswap(a, b, n)进行的是n*2个元素的交换,而swap(a, b)仅仅进行两个元素之间的交换。

vecswap是vector swap的缩写。

vector即向量,表示多个元素

  好了,言归正传。前面说了这么多,事实上都是基础先修课,接下来才是真正的核心代码呦。

_pqsort

回想一下声明部分:

static void
_pqsort(void *a, size_t n, size_t es,
int (*cmp) (const void *, const void *), void *lrange, void *rrange);

  由于a是带排序数字序列的首地址,所以我以下希望能用数组的写法来简化我的描写叙述。

比方&a[1] = (char *) a + es&a[n-1] = (char *) a + (n-1)*es

等号右边的表达式是void *实现C语言泛型功能的典型方法。

诚然,在语法上。二者并非等价的。但在逻辑上是能够理解的。仅仅是为了便于理解,简化叙述

  cmp前面我也提到了是一个回调函数,实现了自己定义的比較操作。这里为了简化叙述。我们假定要完毕的就是一个递增序列,而cmp完毕的就是一般的大小比較操作。

  同样为了便于表述。我们假定我们要完毕的是数字的排序工作,而不是其它自己定义类型的排序工作。

局部变量

    char *pa, *pb, *pc, *pd, *pl, *pm, *pn;
size_t d, r;
int swaptype, cmp_result;

loop循环

loop:   SWAPINIT(a, es);

  这一行使用SWAPINIT宏函数,求解出了swapcode的值。行首有一label(标签)——loop:说明接下来会有一个goto的循环语句。

读者朋友请不要在这里跟我纠结方法论中的论调,我仅仅想说: goto 有时候确实是非常方便的。可读性也不错。

每循环一次完毕的是快排的一趟排序工作

一段冒泡

    if (n < 7) {
for (pm = (char *) a + es; pm < (char *) a + n * es; pm += es)
for (pl = pm; pl > (char *) a && cmp(pl - es, pl) > 0;
pl -= es)
swap(pl, pl - es);
return;
}

  这段代码。假设你使用了我前面简化的数组表示法来代换的话,实际上不难理解。 在带排序元素个数小于7的时候,我们採用 冒泡排序

  在元素个数不多的时候,使用快排反而不能提高效率。倒不如传统的冒泡来的实在。

  然而究竟这个数为什么是7,而不是6。8或其它数字。我也不得而知。

我仅仅能说这就是一个Magic Number中文译为魔数、幻数。指代码中出现的不明所以。意义不明的数字)。

选取模糊中位数

    pm = (char *) a + (n / 2) * es;
if (n > 7) {
pl = (char *) a;
pn = (char *) a + (n - 1) * es;
if (n > 40) {
d = (n / 8) * es;
pl = med3(pl, pl + d, pl + 2 * d, cmp);
pm = med3(pm - d, pm, pm + d, cmp);
pn = med3(pn - 2 * d, pn - d, pn, cmp);
}
pm = med3(pl, pm, pn, cmp);
}
swap(a, pm);

  首先是pm = &a[n/2],在n大于7的时候。 pl =&a[0]; pn = &a[n-1]; 然后在元素个数n大于40的时候:

没错,为什么是40。

这又是一个Magic Number

  又一次选择新的pl,pm,pr。d = (n / 8) * es;我们能够假想将n个数字分成8个子区间。

  • pl是左边三个区间首部中的中位数索引(首部指的是子区间第0个元素)
  • pm是中间三个区间首部中的中位数索引
  • pr是右边三个区间首部中的中位数索引

      接着一个pm = med3(pl, pm, pn, cmp);在这三个中位数中选取中位数。所以最后我们得到的pm实际上是比較接近于整个数字序列中位数的索引。

    当然并非全部数字中的中位数。我们可称它为模糊中位数

      了解快排的过程,我们就会知道每趟排序之前选取一个元素作为基准。排序之后保证该基准左边都小于它。基准的右边都大于它。然后该基准的左右区间在反复这一排序过程。假设我们每趟选取的基准都接近中位数,保证左右区间的长度大致同样。那么接下来排序的效率就更高。

    swap(a, pm);
pa = pb = (char *) a + es;
pc = pd = (char *) a + (n - 1) * es;

  将pm的的值与a[0]的值交互。我们的模糊中位数此时保存在了第一个元素中,接下来我称它为基准

  然后:pa = pb = a[1]; pc = pd = a[n-1];

一趟排序

    for (;;) {
while (pb <= pc && (cmp_result = cmp(pb, a)) <= 0) {
if (cmp_result == 0) {
swap(pa, pb);
pa += es;
}
pb += es;
}
while (pb <= pc && (cmp_result = cmp(pc, a)) >= 0) {
if (cmp_result == 0) {
swap(pc, pd);
pd -= es;
}
pc -= es;
}
if (pb > pc)
break;
swap(pb, pc); //能执行到这一步。说明*pb>*a,*pc<*a。交换一下。
pb += es;
pc -= es;
}

  一个两层循环。涉及代码量较多,这里我简单地介绍一下它的功能。大家努力去自行理解。好吧,事实上是我说累了,懒得说了。它的功能是基本完毕了快排中的一趟排序。唯一的不足之处就是我们的基准还不在中间位置。此外该操作还把序列中和基准a[0]同样的数都交换到了序列的左端和右端的连续区间。

  

  所以接下来我们要把基准区间都交换到中间位置才行。

把基准交换到中间

    pn = (char *) a + n * es; //pn = a[n]...不要操心越界。以下并不会訪问该内存
r = min(pa - (char *) a, pb - pa);
vecswap(a, pb - r, r);
r = min((size_t)(pd - pc), pn - pd - es);
vecswap(pb, pn - r, r);

  这部分代码就是把数字序列左右两端的连续区间(值等于基准)都交换到序列的中间。

之所以调用min()来确定交换的个数r。是由于交换前后两个区间是可能有重合的,所以我们要保证交换的元素个数最少。

以左端的交换为例(黄颜色的部分表示值都等于基准a[0])

  • A图表示pa - (char *) a < pb - pa
  • B图表示pa - (char *) a > pb - pa



      到此为止。我们一趟排序工作完毕了。接下来要做的就是用递归或循环来開始下一趟排序。

開始下一趟排序

简单地描写叙述一下快排过程,在一趟快排结束后。我们要用递归(或循环迭代)的方式在反复排序工作。此后就是在基准左边这一区间展开一趟排序,在基准右边区间也展开一趟排序。这就是分治思想

    if ((r = pb - pa) > es) {
void *_l = a, *_r = ((unsigned char*)a)+r-1;
if (!((lrange < _l && rrange < _l) ||
(lrange > _r && rrange > _r)))
_pqsort(a, r / es, es, cmp, lrange, rrange);
}

  这段代码是对基准左边的区间进行一趟递归的快排。

注意,最外层的if条件中对r进行了又一次赋值(r = pb - pa)。 推断pb - pa这一个区间元素个数是否大于1(仅仅有一个元素显然不须要排序的)。为什么是推断pb - pa而不是推断pa - a呢?直接上图(与前文中的AB两种情况相应):



黄色左边的白色部分。是我们要排序的区间

  接着看代码,内层也嵌套了一个if,他的条件非常复杂。

肢解一下。这个条件有一个非操作。我设该条件为!T,用伪码表示:

// T = ((lrange < _l && rrange < _l)||(lrange > _r && rrange > _r))
if (!T)
_pqsort(...);

  去理解它的逆命题(else): 假设满足条件T。则不会进行排序。事实上非常好理解。lrangerragne是待排序的区间左右边界。而_l_r是基准左側区间的实际左右边界。假设待排序的边界比实际左边界还要小。或者比实际的右边界还要大,显然是不满足条件的。

实际上在整个pqsort.c源代码中,所做的操作差点儿于普通的快排无异,唯一体现了部分快排算法部分二字的地方就是这内层嵌套的循环而已。

    if ((r = pd - pc) > es) {
void *_l, *_r; /* Iterate rather than recurse to save stack space */
a = pn - r;
n = r / es; _l = a;
_r = ((unsigned char*)a)+r-1;
if (!((lrange < _l && rrange < _l) ||
(lrange > _r && rrange > _r)))
goto loop;
}

  这段代码是对基准右边的区间进行了一次快排。其过程和前面类似,就不赘述了。不同之处是关于首元素索引不再是原先的a。而是pn - r,这并不难理解。另外一个变化就是这一趟新排序的開始不是使用的递归,而是循环(goto loop)。作者在凝视中也解释了,没有继续採用递归是为了节省栈空间


pqsort.c源代码

#include <sys/types.h>

#include <errno.h>
#include <stdlib.h> static inline char *med3 (char *, char *, char *,
int (*)(const void *, const void *));
static inline void swapfunc (char *, char *, size_t, int); #define min(a, b) (a) < (b) ? a : b /*
* Qsort routine from Bentley & McIlroy's "Engineering a Sort Function".
*/
#define swapcode(TYPE, parmi, parmj, n) { \
size_t i = (n) / sizeof (TYPE); \
TYPE *pi = (TYPE *)(void *)(parmi); \
TYPE *pj = (TYPE *)(void *)(parmj); \
do { \
TYPE t = *pi; \
*pi++ = *pj; \
*pj++ = t; \
} while (--i > 0); \
} #define SWAPINIT(a, es) swaptype = ((char *)a - (char *)0) % sizeof(long) || \
es % sizeof(long) ? 2 : es == sizeof(long)? 0 : 1; static inline void
swapfunc(char *a, char *b, size_t n, int swaptype)
{ if (swaptype <= 1)
swapcode(long, a, b, n)
else
swapcode(char, a, b, n)
} #define swap(a, b) \
if (swaptype == 0) { \
long t = *(long *)(void *)(a); \
*(long *)(void *)(a) = *(long *)(void *)(b); \
*(long *)(void *)(b) = t; \
} else \
swapfunc(a, b, es, swaptype) #define vecswap(a, b, n) if ((n) > 0) swapfunc((a), (b), (size_t)(n), swaptype) static inline char *
med3(char *a, char *b, char *c,
int (*cmp) (const void *, const void *))
{ return cmp(a, b) < 0 ? (cmp(b, c) < 0 ? b : (cmp(a, c) < 0 ? c : a ))
:(cmp(b, c) > 0 ? b : (cmp(a, c) < 0 ? a : c ));
} static void
_pqsort(void *a, size_t n, size_t es,
int (*cmp) (const void *, const void *), void *lrange, void *rrange)
{
char *pa, *pb, *pc, *pd, *pl, *pm, *pn;
size_t d, r;
int swaptype, cmp_result; loop: SWAPINIT(a, es);
if (n < 7) {
for (pm = (char *) a + es; pm < (char *) a + n * es; pm += es)
for (pl = pm; pl > (char *) a && cmp(pl - es, pl) > 0;
pl -= es)
swap(pl, pl - es);
return;
}
pm = (char *) a + (n / 2) * es;
if (n > 7) {
pl = (char *) a;
pn = (char *) a + (n - 1) * es;
if (n > 40) {
d = (n / 8) * es;
pl = med3(pl, pl + d, pl + 2 * d, cmp);
pm = med3(pm - d, pm, pm + d, cmp);
pn = med3(pn - 2 * d, pn - d, pn, cmp);
}
pm = med3(pl, pm, pn, cmp);
}
swap(a, pm);
pa = pb = (char *) a + es; pc = pd = (char *) a + (n - 1) * es;
for (;;) {
while (pb <= pc && (cmp_result = cmp(pb, a)) <= 0) {
if (cmp_result == 0) {
swap(pa, pb);
pa += es;
}
pb += es;
}
while (pb <= pc && (cmp_result = cmp(pc, a)) >= 0) {
if (cmp_result == 0) {
swap(pc, pd);
pd -= es;
}
pc -= es;
}
if (pb > pc)
break;
swap(pb, pc);
pb += es;
pc -= es;
} pn = (char *) a + n * es;
r = min(pa - (char *) a, pb - pa);
vecswap(a, pb - r, r);
r = min((size_t)(pd - pc), pn - pd - es);
vecswap(pb, pn - r, r);
if ((r = pb - pa) > es) {
void *_l = a, *_r = ((unsigned char*)a)+r-1;
if (!((lrange < _l && rrange < _l) ||
(lrange > _r && rrange > _r)))
_pqsort(a, r / es, es, cmp, lrange, rrange);
}
if ((r = pd - pc) > es) {
void *_l, *_r; /* Iterate rather than recurse to save stack space */
a = pn - r;
n = r / es; _l = a;
_r = ((unsigned char*)a)+r-1;
if (!((lrange < _l && rrange < _l) ||
(lrange > _r && rrange > _r)))
goto loop;
}
/* qsort(pn - r, r / es, es, cmp);*/
} void
pqsort(void *a, size_t n, size_t es,
int (*cmp) (const void *, const void *), size_t lrange, size_t rrange)
{
_pqsort(a,n,es,cmp,((unsigned char*)a)+(lrange*es),
((unsigned char*)a)+((rrange+1)*es)-1);
}

具体解释Redis源代码中的部分高速排序算法(pqsort.c)的更多相关文章

  1. java:高速排序算法与冒泡排序算法

     Java:高速排序算法与冒泡算法 首先看下,冒泡排序算法与高速排序算法的效率: 例如以下的是main方法: /**   *  * @Description:  * @author:cuiyaon ...

  2. 高速排序算法C++实现

    //quick sort //STL中也有现成的高速排序算法.内部实现採用了下面技巧 //1)枢轴的选择採取三数取中的方式 //2)后半段採取循环的方式实现 //3)高速排序与插入排序结合 #incl ...

  3. 【CSWS2014 Summer School】互联网广告中的匹配和排序算法-蒋龙(下)

    [CSWS2014 Summer School]互联网广告中的匹配和排序算法-蒋龙(上) Fig19,用到了矩阵,这个我没有听太明白,蒋博士也没有详细说明.不过可以明确的一点就是,我们常说的K-mea ...

  4. 编程算法 - 高速排序算法 代码(C)

    高速排序算法 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 经典的高速排序算法, 作为一个编程者, 不论什么时候都要完整的手写. 代码: /* * m ...

  5. Java中几种常见排序算法

    日常操作中常见的排序方法有:冒泡排序.快速排序.选择排序.插入排序.希尔排序等. 冒泡排序是一种简单的排序算法.它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来.走访数 ...

  6. Java 中常见的各种排序算法汇总

    首先,Java中自已是有排序的 说明:(1)Arrays类中的sort()使用的是“经过调优的快速排序法”;(2)比如int[],double[],char[]等基数据类型的数组,Arrays类之只是 ...

  7. Java中的数据结构及排序算法

    (明天补充) 主要是3种接口:List Set Map List:ArrayList,LinkedList:顺序表ArrayList,链表LinkedList,堆栈和队列可以使用LinkedList模 ...

  8. python实现高速排序算法(两种不同实现方式)

    # -*- coding: utf-8 -*- """ Created on Fri May 16 17:24:05 2014 @author: lifeix " ...

  9. 面试中常用的六种排序算法及其Java实现

    常见排序算法的时间复杂度以及稳定性: 1 public class Sort { public static void main(String[] args){ int[] nums=new int[ ...

随机推荐

  1. c18---数组和指针

    // // main.c // day09 #include <stdio.h> int main(int argc, const char * argv[]) { ; int *numP ...

  2. 轻快的vim(四):修改

    我想每个Coder都深刻的明白,修改这一操作在代码的世界里是多么重要 与其说修改,无非就是删除了再插入,但VIM把这两者结合的很有效率 闲话少说,让我们再次使用这轻快的VIM在code上起舞 字符替换 ...

  3. ubuntu之修改ls显示颜色

    Linux 系统中 ls 文件夹的痛苦我就不说了,为了不伤眼睛,一般 ssh 终端背景都用的黑色,文件夹又是你妈的深蓝色,每次看文件夹都要探头仔细去看.这下彻底解决这个问题.   因为ubuntu下的 ...

  4. Linux内核OOM机制的详细分析【转】

    本文转载自:http://blog.csdn.net/liukuan73/article/details/43238623 Linux内核根据应用程序的要求分配内存,通常来说应用程序分配了内存但是并没 ...

  5. Linux - 进程管理,ps与top

    一个运行的程序,可能有多个进程. PID进程ID. UID启动进程的ID. 进程所属组GID. 进程的状态R运行.S睡眠.Z僵尸. 父进程管理子进程,父进程终止的时候子进程也会终止. 常用的组合为: ...

  6. hihoCoder-1829 2018亚洲区预选赛北京赛站网络赛 B.Tomb Raider 暴力 字符串

    题面 题意:给你n个串,每个串都可以选择它的一个长度为n的环形子串(比如abcdf的就有abcdf,bcdfa,cdfab,dfabc,fabcd),求这个n个串的这些子串的最长公共子序列(每个串按顺 ...

  7. 原生JS通过勾股定理计算苹果菜单效果

    JS原生苹果菜单效果 知识点: 勾股定理 a²+b²=c² Event 是一个事件对象,当一个事件发生后,和当前事件发生相关的详细信息会被临时存储到一个指定的地方,也就是event对象,以方便我们在需 ...

  8. 项目中遇到的所有ECharts图表集合

    全放在了ECharts官网示例里面以后会一直往里面添加: https://gallery.echartsjs.com/explore.html?u=bd-2133619855&type=wor ...

  9. guice整合struts2,guice的使用(八)

    平时我们习惯用了spring整合struts2,今天我们就来见识一下guice整合struts2吧. 看web.xml配置: <?xml version="1.0" enco ...

  10. Linq怎么支持Monad

    在上一篇创建了我们的第一个Monad, Identity<T>. 我们确定了类型要变成Monad, 它必须有一个type constructor(Identity<T>), 和 ...