具体解释Redis源代码中的部分高速排序算法(pqsort.c)
看标题。你可能会疑惑:咦?你这家伙。怎么不解说完整的快排,仅仅讲一部分快排……-。-
哎,冤枉。
“部分快排”是算法的名字。实际上本文相当具体呢。本文差点儿与普通快排无异。看懂了本文,你对普通的快排也会有更深的认识了。
高速排序算法(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
这是个宏函数 。其功能是将以parmi
和parmj
为首地址的n个字节进行交换。
#define swapcode(TYPE, parmi, parmj, n) { \
- 形參
TYPE
就是指的类型。阅读后面代码。可知其实參是char
和long
这两种。 - 形參
n
指定的是待交换字节数。
请同意我在宏这里。使用了术语:形參、实參。尽管可能不搭,但目的是便于读者理解。
size_t i = (n) / sizeof (TYPE); \
TYPE *pi = (TYPE *)(void *)(parmi); \
TYPE *pj = (TYPE *)(void *)(parmj); \
i
就是指定类型(char或long)的元素的个数。然后将參数parmi
和parmj
转换成指定的类型的指针pi
和pj
。
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
。则不会进行排序。事实上非常好理解。lrange
。rragne
是待排序的区间左右边界。而_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)的更多相关文章
- java:高速排序算法与冒泡排序算法
Java:高速排序算法与冒泡算法 首先看下,冒泡排序算法与高速排序算法的效率: 例如以下的是main方法: /** * * @Description: * @author:cuiyaon ...
- 高速排序算法C++实现
//quick sort //STL中也有现成的高速排序算法.内部实现採用了下面技巧 //1)枢轴的选择採取三数取中的方式 //2)后半段採取循环的方式实现 //3)高速排序与插入排序结合 #incl ...
- 【CSWS2014 Summer School】互联网广告中的匹配和排序算法-蒋龙(下)
[CSWS2014 Summer School]互联网广告中的匹配和排序算法-蒋龙(上) Fig19,用到了矩阵,这个我没有听太明白,蒋博士也没有详细说明.不过可以明确的一点就是,我们常说的K-mea ...
- 编程算法 - 高速排序算法 代码(C)
高速排序算法 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 经典的高速排序算法, 作为一个编程者, 不论什么时候都要完整的手写. 代码: /* * m ...
- Java中几种常见排序算法
日常操作中常见的排序方法有:冒泡排序.快速排序.选择排序.插入排序.希尔排序等. 冒泡排序是一种简单的排序算法.它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来.走访数 ...
- Java 中常见的各种排序算法汇总
首先,Java中自已是有排序的 说明:(1)Arrays类中的sort()使用的是“经过调优的快速排序法”;(2)比如int[],double[],char[]等基数据类型的数组,Arrays类之只是 ...
- Java中的数据结构及排序算法
(明天补充) 主要是3种接口:List Set Map List:ArrayList,LinkedList:顺序表ArrayList,链表LinkedList,堆栈和队列可以使用LinkedList模 ...
- python实现高速排序算法(两种不同实现方式)
# -*- coding: utf-8 -*- """ Created on Fri May 16 17:24:05 2014 @author: lifeix " ...
- 面试中常用的六种排序算法及其Java实现
常见排序算法的时间复杂度以及稳定性: 1 public class Sort { public static void main(String[] args){ int[] nums=new int[ ...
随机推荐
- Hadoop Web项目--Friend Find系统
项目使用软件:Myeclipse10.0,JDK1.7,Hadoop2.6,MySQL5.6.EasyUI1.3.6.jQuery2.0,Spring4.1.3. Hibernate4.3.1,str ...
- poj 1321(DFS)
在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别.要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C. ...
- 表格td内容过多时,td显示省略号,鼠标移入显示全部内容。
转自:https://blog.csdn.net/weixin_42193908/article/details/80405014 两种方式显示: 1.title方式显示: <!DOCTYPE ...
- php和nodejs
整个故事正如好莱坞大片的经典剧情走向:两位昔日好友如今分道扬镳,甚至被迫陷入了你死我活的斗争当中.刚开始的分歧并不严重,无非是一位老友对于另一位伙伴长久以来占据.但又绝口不提的业务领域产生了点兴趣.而 ...
- 922. 按奇偶排序数组 II
给定一个非负整数数组 A, A 中一半整数是奇数,一半整数是偶数. 对数组进行排序,以便当 A[i] 为奇数时,i 也是奇数:当 A[i] 为偶数时, i 也是偶数. 你可以返回任何满足上述条件的数组 ...
- 前端分页功能实现(PC)
<!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>加 ...
- Super超级ERP系统---(7)货位管理
货位是ERP系统总的仓库管理中必不可少的,仓库是有货架组成,货架上的每个格子就是一个货位,所有货位上的商品的库存总和就是仓库商品的库存.仓库的货位主要分为货架和托盘,货架就是仓库的固定货位,托盘就是移 ...
- WinForm和数据库的连接
有几天没有写东西,今天来写点关于数据库的东西. 第一步:现在你自己的SQL Server数据库中创建一个新的数据库test,然后在里面新建一张表tb_user,在这张表中添加几个字段并为它赋值,具体结 ...
- 第5章分布式系统模式 使用服务器激活对象通过 .NET Remoting 实现 Broker
正在使用 Microsoft? .NET Framework 构建一个需要使用分布式对象的应用程序.您的要求包括能够按值或按引用来传递对象,无论这些对象驻留在同一台计算机上,还是驻留在同一个局域网 ( ...
- javascript 将单词首字母大写,其余小写
// 1 别人写的,我拿来参考了一下 function titleCase(str) { var array = str.toLowerCase().split(" "); for ...