Python Skiplistpython insert into跳表插入

& leveldb中的Skip list(跳跃表)
leveldb中的Skip list(跳跃表)
这段时间在关注。leveldb中有一个核心的数据结构skiplist,如图所示skip list和单链表类似,只不过有些节点有前向指针以便加快遍历,有k个前向指针的节点叫做level k node。
本博客主要介绍skiplist的算法原理,包括skiplist增删改查,下一篇博客将介绍skiplist的复杂度分析。(博客内容主要是翻译)
Skip list(跳跃表)是一种可以代替平衡树的数据结构。Skip lists应用概率保证平衡,平衡树采用严格的旋转(比如平衡二叉树有左旋右旋)来保证平衡,因此Skip list比较容易实现,而且相比平衡树有着较高的运行效率。
从概率上保持数据结构的平衡比显示的保持数据结构平衡要简单的多。对于大多数应用,用skip list要比用树更为自然,算法也会相对简单。由于skip list比较简单,实现起来会比较容易,虽然和平衡树有着相同的时间复杂度(O(logn)),但是skip list的常数项会相对小很多。skip list在空间上也比较节省。一个节点平均只需要1.333个指针(甚至更少),并且不需要存储报纸平衡的变量。
Skip Lists
如图链表中的值,非递减顺序排列。
图a:为了查找单链表中的某个值,最坏情况下需要将链表全部遍历一遍,需要遍历n个节点。
图b:每2个节点存储了它后面第2个节点,知识最多需要遍历n/2 + 1个节点。
图c:图b基础上每4个节点存储前面第4个节点内容,这时最多遍历n/4 + 2个节点。(n/4 + 4/2)
图d:如果每2^i个节点都指向前面2^i个节点,寻找一个节点的复杂度变成logn(类似于二分查找)。虽然这种结构查找很快但是插入删除却很复杂。
有着k个前向指针(farword pointers)的节点叫做level k node。如果每2^i的节点指向前面2^i个后继节点,那么节点的分布情况为:50% 在第一层,25%在第二层,12.5%在第3层。如果所有节点的层数是随机挑选的。节点第i个前向指针指向后面第2^(i-1)个节点。插入和删除只需要局部修改少数指针,节点的层数(level)在插入时随机选取,并且以后不需要修改。虽然有一些指针的排列会导致很坏的运行时间,但是这些情况很少出现。
首先申请一个NIL节点,此节点的Key赋一个最大值作为哨兵节点。
链表的level置为1,头结点所有的forward pointer指向NIL节点。
为了找到要查找的值,我们逐次遍历forward pointer。
当指针在level 1层不能继续前进时,我们肯定在需要节点的前一个节点处(如果链表中存在要查找的节点)
Search(list, searchKey)
x := list-&header
//loop invariant: x-&key & searchKey
for i := list-&level downto 1 do
while x-&forward[i]-&key & searchKey do
x := x-&forward[i]
//x-&key & sarchKey &= x-&forward[1]-&key
x := x-&forward[1]
if x-&key = searchKey then rturn x-&value
else return failure
随机选择层数
之前讨论时层数的选择是按照1/2(p=1/2)的概率选择的,p可以取[0, 1)间的任意值,算法如下所示。
randomLevel()
//random()that returns a random value in [0..1)
while random() & p and |v| & MaxLevel do
|v| := |v| + 1
return |v|
Insert(list, searchKey, newValue)
local update[1..MaxLevel]
x : =list-&header
for i := list-&level donwto 1 do
while x-&forward[i]-&key & searchKey do
x := x-&forward[i]
//x-&key & searchKey &= x-&forward[i]-&key
update[i] := x
x := x-&forward[1]
if x-&key = searchKey then x-&value := newValue
|v| := randomLevel()
if |v| & list-&level
for i := list-&level + 1 to |v| do
update[i] := list-&header
list-&level := |v|
x := makeNode(|v|, searchKey, value)
for i := 1 to level do
x-&forward[i] := update[i]-&forward[i]
update[i]-&forward[i] := x
Delete(list searchKey)
local update[1..MaxLevel]
x := list-&header
for i := list-&level downto 1 do
while x-&forward[i]-&key & searchKey do
x := x-&forward[i]
update[i] := x
x := x-&forward[1]
if x-&key = searchKey then
for i := 1 to list-&level do
if update[i]-&forward[i] != x then break
update[i]-&forward[i] := x-&forward[i]
free(x)
while list-&leve & 1 and list-&header-&forward[list-&level] = NULL do
list-&level := list-&level - 1
从理论的角度看,skiplist是完全没有必要的。Skip lists能做的事情平衡树也同样能做,并且在最坏情况下的时间复杂度比Skip lists要好。但是实现平衡树却是一项复杂的工作,除了在数据结构课程上实现平衡树外,实际应用中很少会实现它。
作为一种简单的数据结构,在大多数应用中Skip lists能够代替平衡树。Skip lists算法非常容易实现、扩展和修改。Skip lists和进行过优化的平衡树有着同样高的性能,Skip lists的性能远远超过未经优化的平衡二叉树。
Related posts:
Categories: , ,上篇中(), 我对跳表的一些基本的概念及其在redis源代码中的定义形式做了一些分析,同时也对跳表为什么能实现“跳跃”功能做了一番介绍。在这篇中,我将重点针对redis中跳表的实现做详细的分析;关于跳表相关的定义以及代码位置,在上篇中已有说明。
我们接着上篇末尾列出的那几个函数定义开始,这几个函数包含了最基本的几种操作,创建、插入、删除、查询;首先,我们看redis中是如何实现跳表的创建的:
typedef struct zskiplist {
struct zskiplistNode *header, *
zskiplist *zslCreate(void) {
zskiplist *
zsl = zmalloc(sizeof(*zsl));
zsl-&level = 1;
zsl-&length = 0;
zsl-&header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
for (j = 0; j & ZSKIPLIST_MAXLEVEL; j++) {
zsl-&header-&level[j].forward = NULL;
zsl-&header-&level[j].span = 0;
zsl-&header-&backward = NULL;
zsl-&tail = NULL;
首先是跳表结构的定义
struct zskiplistNode *header, *tail
头尾指针,表名是双向链表。
length是表示链表中节点的个数。
表示跳表的层数。
然后我们在看zslCreate函数的定义,其中最关键的就是里边的那个循环,
ZSKIPLIST_MAXLEVEL
宏定义,用于表示这个跳表最多有多少层。在循环内,将指向后边节点的forward指针置为NULL。
zslCreate调用完成后,其实就是创建了跳表的头结点(我们知道所有的链表实现中通常也是需要一个头结点的)。
然后我们再来看如何给跳表插入一个元素,插入元素代码请看, 这里不贴出完整的代码,我只取其中比较重要的部分分析。
插入一个元素到跳表中,首先需要确定的是插入元素的位置,插入元素的时候需要保证跳表元素的排列顺序是升序排列;然后确定待插入节点的层数;最后是将节点各层指向正确的后续节点,然后插入元素。
首先请看下述代码段:
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
for (i = zsl-&level-1; i &= 0; i--) {
/* store rank that is crossed to reach the insert position */
rank[i] = i == (zsl-&level-1) ? 0 : rank[i+1];
while (x-&level[i].forward &&
(x-&level[i].forward-&score & score ||
(x-&level[i].forward-&score == score &&
compareStringObjects(x-&level[i].forward-&obj,obj) & 0))) {
rank[i] += x-&level[i].
x = x-&level[i].
update[i] =
这段代码主要的作用就是查找节点所要插入的位置。首先请看下边这幅图:
通常,在跳表中查找元素的时候,不像在链表中查找元素那样需要遍历,而是会从头结点(头结点的下一节点才是元素节点)的最顶层开始,即上述代码的for循环,如果level数组的forward指针指向的节点的score值大于要插入的score,那么就下降一层;否则,就把x前进一个节点,指向到下一个节点,继续比较,即上述代码中while循环所做的工作。最后,当结束for循环时,update数组中保存的节点的forward就是将要插入的节点的level数组中的forward需要的值,并且待插入的节点一定是位于update[0]这个指针所指节点的后边。
当新节点需要插入的位置找到后,就需要确定新节点的层数:
level = zslRandomLevel();
我们知道,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以节点层数的确定当然也是随机的,以上这行代码便是确定节点的层数。
因为在前边查找的时候,是从当前跳表的最高一层开始查找的,如果新节点的层数大于跳表当前的层数,则需要更新跳表的层数并扩展之前的update数组,代码如下:
if (level & zsl-&level) {
for (i = zsl-& i & i++) {
rank[i] = 0;
update[i] = zsl-&
update[i]-&level[i].span = zsl-&
zsl-&level =
这里需要指出的是为什么新加的层节点直接用zsl-&header赋值呢? 原因是这样的,因为新加入的这层与zsl-&header直接肯定是没有其他节点的层的;而下边我们在给初始化新插入节点的level数组的时候,是把update的每一个元素当作其前一个跳跃节点的。
x = zslCreateNode(level,score,obj);
for (i = 0; i & i++) {
x-&level[i].forward = update[i]-&level[i].
update[i]-&level[i].forward =
/* update span covered by update[i] as x is inserted here */
x-&level[i].span = update[i]-&level[i].span - (rank[0] - rank[i]);
update[i]-&level[i].span = (rank[0] - rank[i]) + 1;
综合这几段代码,我们便可以知道update这个数组的作用了;然后便是给backward赋值,以及修改zsl的tail指针:
x-&backward = (update[0] == zsl-&header) ? NULL : update[0];
if (x-&level[0].forward)
x-&level[0].forward-&backward =
zsl-&tail =
zsl-&length++;
到这里,基本跳表的创建以及插入元素都分析完毕了,下边,我们看看如何删除一个元素。删除元素的代码位于
同插入节点类似,删除节点的时候,同样也是需要先找到需要删除节点的位置,代码如下:
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
for (i = zsl-&level-1; i &= 0; i--) {
while (x-&level[i].forward &&
(x-&level[i].forward-&score & score ||
(x-&level[i].forward-&score == score &&
compareStringObjects(x-&level[i].forward-&obj,obj) & 0)))
x = x-&level[i].
update[i] =
这里,需要特别注意的是,到for循环结束时,update[0]处的指针所指向的是要删除的节点的前一个节点,到这里为止,要删除的节点是否存在并不知道。
x = x-&level[0].
这里便是将节点往后前进一个位置,便到了我们要寻找的节点的位置,到这里为止,要删除的节点是否存在仍然不知道,下边便是做节点的比较:
if (x && score == x-&score && equalStringObjects(x-&obj,obj)) {
zslDeleteNode(zsl, x, update);
zslFreeNode(x);
return 0; /* not found */
只有当score值与节点元素的值全部相等时,才说明要删除的节点是存在的,否则就是不存在的。
最后,节点元素的查找跟之前插入跟删除节点的查找过程是一样的,具体请看redis的实现
这个函数。
本文目前还没有评论……google 的 nosql leveldb就是使用了 skip lists
【复杂度分析】& 一个数据结构的好坏大部分取决于它自身的空间复杂度以及基于它一系列操作的时间复杂度。跳跃表之所以被誉为几乎能够代替平衡树,其复杂度方面自然不会落后。我们来看一下跳跃表的相关复杂度:& 空间复杂度:&O(n)&&&&(期望)& 跳跃表高度:&O(logn)&&(期望)& 相关操作的时间复杂度:& &查找:&&O(logn)&&(期望)& &插入:&&&O(logn)&&(期望)& &删除:&&O(logn)&&(期望)& && 之所以在每一项后面都加一个&期望&,是因为跳跃表的复杂度分析是基于概率论的。有可能会产生最坏情况,不过这种概率极其微小。&一、&空间复杂度分析&&O(n)& 假设一共有n个元素。根据性质1,每个元素插入到第i层(Si)的概率为pi-1&,则在第i层插入的期望元素个数为npi-1,跳跃表的元素期望个数为&&,当p取小于0.5的数时,次数总和小于2n。& 所以总的空间复杂度为O(n)& 二、跳跃表高度分析&&O(logn)& 根据性质1,每个元素插入到第i层(Si)的概率为p^i&,则在第i层插入的期望元素个数为np^(i-1)。& 考虑一个特殊的层:第1+&层。& &层的元素期望个数为&np^0+np^1+...+np^(h-1),当n取较大数时,这个式子的值接近0,故跳跃表的高度为O(logn)级别的。& 三、查找的时间复杂度分析&&O(logn)& 我们采用逆向分析的方法。假设我们现在在目标节点,想要走到跳跃表最左上方的开始节点。这条路径的长度,即可理解为查找的时间复杂度。& 设当前在第i层第j列那个节点上。& i)&如果第j列恰好只有i层(对应插入这个元素时第i次调用随机化模块时所产生的B决策,概率为1-p),则当前这个位置必然是从左方的某个节点向右跳过来的。& ii)&如果第j列的层数大于i(对应插入这个元素时第i次调用随机化模块时所产生的A决策,概率为p),则当前这个位置必然是从上方跳下来的。(不可能从左方来,否则在以前就已经跳到当前节点上方的节点了,不会跳到当前节点左方的节点)& 设C(k)为向上跳k层的期望步数(包括横向跳跃)& 有:& C(0)&=&0& C(k)&=&(1-p)(1+向左跳跃之后的步数)+p(1+向上跳跃之后的步数)& &&&&&=&(1-p)(1+C(k))&+&p(1+C(k-1))& C(k)&=&1/p&+&C(k-1)& C(k)&=&k/p& &而跳跃表的高度又是logn级别的,故查找的复杂度也为logn级别。& 对于记忆化查找(Search&with&fingers)技术我们可以采用类似的方法分析,很容易得出它的复杂度是O(logk)的(其中k为此次与前一次两个被查找元素在跳跃表中位置的距离)。& 四、插入与删除的时间复杂度分析&&O(logn)& &插入和删除都由查找和更新两部分构成。查找的时间复杂度为O(logn),更新部分的复杂度又与跳跃表的高度成正比,即也为O(logn)。& &所以,插入和删除操作的时间复杂度都为O(logn)& 最后,概率因子一般用1/2或1/e&
为什么选择跳表
目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等。
想象一下,给你一张草稿纸,一只笔,一个编辑器,你能立即实现一颗红黑树,或者AVL树
出来吗? 很难吧,这需要时间,要考虑很多细节,要参考一堆算法与数据结构之类的树,
还要参考网上的代码,相当麻烦。
用跳表吧,跳表是一种随机化的数据结构,目前开源软件 Redis 和 LevelDB 都有用到它,
它的效率和红黑树以及 AVL 树不相上下,但跳表的原理相当简单,只要你能熟练操作链表,
就能轻松实现一个 SkipList。
自己实现的(还有点问题)
#ifndef _SKIPLIST_H_
#define _SKIPLIST_H_
#define MAXLEVEL 15
#define MAX_KEY_SIZE 65535
typedef struct node
struct node*
struct level
struct node*
#endif // 0
typedef struct node
struct node** // 这里二级指针 一级是next 二级是[] 先找到层level再next
typedef struct skiplist
unsigned long
}skiplist_t;
skiplist_t* skiplist_create();
void skiplist_free(skiplist_t* skl);
node_t* skiplist_createNode(int level, int val);
int skiplist_randomLevel(void);
node_t* skiplist_insert(skiplist_t *skl, int value);
void skl_deleteNode(skiplist_t* skl, node_t* del, node_t** update); // update[]保存了要删除节点的各层情况
int skl_delete(skiplist_t* skl, int value);
void skl_search(skiplist_t* skl, int value);
void skl_foreach(skiplist_t* skl);
#endif // _SKIPLIST_H_
#include &malloc.h&
#include &assert.h&
#include &stdio.h&
#include &stdlib.h&
#include "skiplist.h"
skiplist_t* skiplist_create()
skiplist_t* skl = (skiplist_t*)malloc(sizeof(skiplist_t));
skl-&level = 1;
skl-&length = 0;
skl-&header = skiplist_createNode(MAXLEVEL, 0); // 创建头结点 它不保存实际的数据
int i = 0;
for(i = 0; i & MAX_PATH; ++i)
skl-&header-&forward[i] = NULL; // 将头结点的每一层forward指针置空 这是横向的
void skiplist_free(skiplist_t* skl)
node_t* node = skl-&header-&forward[0];
free(skl-&header); // 释放头
while(node)
next = node-&forward[0]; // 横向的next 0层保存了所有的数据 其他层都是部分数据
free(node);
free(skl);
node_t* skiplist_createNode(int levl, int val)
node_t* node = (node_t*)malloc(sizeof(node_t) + levl * sizeof(node_t*));
// node_t* node = (node_t*)malloc(sizeof(node_t));
// 这里分配每层的指针空间来保forward地址
assert(node);
node-&val =
// node-&forward = (node_t**)(levl * sizeof(node_t*));
node-&forward = (node_t**)(node + 1);
printf("node create\n");
// printf("\n");
int skiplist_randomLevel(void)
int level = 1;
while(rand() % 2)
level += 1;
return (level & MAXLEVEL) ? level : MAXLEVEL; // 有些node是在一些层中没有的 没一层都是只有部分结点
// 都是有序元素的排列 按key的大小排序
// 确定该元素要占据的层数 K(采用丢硬币的方式,这完全是随机的)
// 然后在 Level 1 ... Level K 各个层的链表都插入元素。
// 如果 K 大于链表的层数,则要添加新的层。
node_t* skiplist_insert(skiplist_t *skl, int value)
node_t* update[MAXLEVEL]; // // update 数组记录了该node在每一层的插入情况 插入还是不插入
curr = skl-&
int level = skl-&
1.从最高层往下查找需要插入的位置 填充update 最上层是level-1
int i = level - 1;
for(; i &= 0; --i) // 遍历每一层
在每一层中查找合适的位置插入
while(curr-&forward[i] && curr-&forward[i]-&val & value) // while就是一个普通链表的遍历
curr = curr-&forward[i];
update[i] = // update数组 记录了 每层中是否插入node 循环结束curr-&forward大于等于node(value)
// 所以是将node插插入了curr-&forward的前面 就是updaute(curr)的后面 即这2个之间
2.产生一个随机层数level 新建一个待插入节点tmp 一层一层插入 更新跳表的level
level = skiplist_randomLevel();
if(skl-&level & level) // 如果level大于skl的level需要添加新的层
int i = skl-&
for(; i & ++i) // 新添加的层中update标价为header和不插入一样
update[i] = skl-&
skl-&level = // 更新level
node_t* node = skiplist_createNode(level, value); // 这里node重新赋值 保存了新的节点 update实际上是找到的大于等于node的节点
printf("new node value is %d \n", node-&val);
3.逐层更新节点的指针 和普通链表插入一样
for(i = 0; i & ++i)
// 在update后面插入new node
node-&forward[i] = update[i]-&forward[i]; // 这里是链表的插入 forward就是next
update[i]-&forward[i] = // 结果:update-&node-&forward 循环level将每层的节点指针都连好 插入新节点
skl-&length++;
// 在各个层中找到包含 x 的节点,使用标准的 delete from list 方法删除该节点
int skl_delete(skiplist_t* skl, int value)
node_t* update[MAXLEVEL];
node_t* node = skl-&
int i = skl-&level - 1;
for(; i &= 0; --i)
// 这里的node也是呈现出楼梯状的查找 删除
while((node-&forward[i]) && (node-&forward[i]-&val & value)) // 找到每层中不小于value的node跳出循环
node = node-&forward[i]; // forward就是next
update[i] = // update记录了 要删除元素的前一个元素 在每层中的信息
node = node-&forward[0]; // 每次循环都是node-&forward的val做对比 所以循环结束 应该是node-&forward结点与value的关系
if(node && value == node-&val) // node-&val大于等于value 相等删除node
skl_deleteNode(skl, node, update); // update记录了 要删除元素的前一个元素 在每层中的信息
free(node);
// 在各个层中找到包含 x 的节点,使用标准的 delete from list 方法删除该节点
void skl_deleteNode(skiplist_t* skl, node_t* del, node_t** update) // update[]保存了要删除节点的各层情况
int i = 0;
for(; i & skl-& ++i) //遍历删除节点的各层 删除各层节点
if(del == update[i]-&forward[i]) // update记录了 要删除元素的前一个元素 在每层中的信息 即update-&node-&forwrad
update[i]-&forward[i] = del-&forward[i]; // next指针的变化
while(skl-&level & 1 && NULL == skl-&header-&forward[skl-&level-1]) // 如果最上层为空 删除层
skl-&level--;
skl-&length--;
void skl_search(skiplist_t* skl, int value)
node_t* node = skl-&
int i = skl-&level - 1;
for(; i &= 0; --i) // 从最高层开始查找 呈现的是梯状查找
while(node-&forward[i] && node-&forward[i]-&val & value) // 头部的forward 开始 直到等于大于
node = node-&forward[i];
// 这里是查找的关键
// 1.开始从顶层查找 到node节点
// 2.接下来一层 从node开始向后查找 就这样一层一层 不断向后查找 呈现一个楼梯状的查找过程
node = node-&forward[0]; // 循环结束node-&forward-&val可能和value相等或者大于value
if(node && value == node-&val) // 相等打印
printf("find is %d \n", node-&val);
printf("not find\n");
void skl_foreach(skiplist_t* skl)
int i = 0;
for(; i & MAXLEVEL; ++i)
printf("level[%d]: ", i);
node = skl-&header-&forward[i]; // 从第0层开始
while(node)
printf("%d -& ", node-&val);
node = node-&forward[i]; // node = node -& next
printf("\n");
printf("\n\n");
#include &stdio.h&
#include "skiplist.h"
int main()
skiplist_t* skl = skiplist_create();
int i = 1;
for(; i &= 6; ++i)
skiplist_insert(skl, i*2);
printf("after insert\n");
skl_foreach(skl);
printf("after delete\n");
skl_delete(skl, 4);
skl_foreach(skl);
printf("after find\n");
skl_search(skl, 2);
skiplist_free(skl);
阅读(...) 评论()开发语言和框架 Archive
是Bjarne Stroustrup教授用于解决资源分配而发明的技术,资源获取即初始化。
RAII是C++的构造机制的直接使用,即利用构造函数分配资源,利用析构函数来回收资源。
我们知道,在C/C++语言中,对动态分配的内存的处理必须十分谨慎。在没有RAII应用的情况下,如果在内存释放之前就离开指针的作用域,这时候几乎没机会去释放该内存,除非垃圾回收器对其管制,否则我们要面对的将会是内存泄漏。
举个例子来说明下RAII在内存分配方面的使用。
struct ByteArray {
unsigned char* data_;
int length_;
void create_bytearray(ByteArray*, int length);
void destroy_bytearray(ByteArray*);
void bar() {
create_bytearray(&ba, 2048);
/* 使用 */
/* 如果有异常,Oops
destroy_bytearray(&ba);
12345678910111213141516
struct ByteArray {&&&&unsigned char* data_;&&&&int length_;};&void create_bytearray(ByteArray*, int length);void destroy_bytearray(ByteArray*);&void bar() {&&&&ByteArray ba;&&&&create_bytearray(&ba, 2048);&&&&/* 使用 */&&&&/* 如果有异常,Oops&&*/&&&&...&&&&destroy_bytearray(&ba);}
这是典型的C风格代码,没有应用RAII。
因此值得注意的是,destroy_bytearray必须在退出作用域前被调用。
然而在复杂的逻辑设计中,程序员往往要花大量的精力以确认所有在该作用域分配的ByteArray得到正确的释放。
- 44,022次 - 32,572次 - 26,890次 - 23,032次 - 18,423次 - 17,536次 - 17,162次 - 16,470次 - 16,447次 - 15,089次的原创经验被浏览,获得 ¥0.003 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.002 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.005 收益
的原创经验被浏览,获得 ¥0.005 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.005 收益
的原创经验被浏览,获得 ¥0.005 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.005 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.005 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.005 收益
的原创经验被浏览,获得 ¥0.001 收益
的原创经验被浏览,获得 ¥0.005 收益
的原创经验被浏览,获得 ¥0.001 收益}

我要回帖

更多关于 python insert into 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信