typedef struct rax {
raxNode *head; //指向头结点的指针
uint64_t numele; //元素个数(key的个数)
uint64_t numnodes; //节点个数
} rax; //rax代表一个Rax树
typedef struct raxNode {
uint32_t iskey:1; //当前节点是否包含一个key,占用一个字节
uint32_t isnull:1; //当前key对应的value是否为空,占用一个字节
uint32_t iscompr:1;//当前节点是否为压缩节点,占用一个字节
uint32_t size:29; //压缩节点压缩的字符串长度 或 非压缩节点的子节点个数,占用29个字节
//真抠,不错
/* Data layout is as follows:
-
If node is not compressed we have ‘size’ bytes, one for each children
-
character, and ‘size’ raxNode pointers, point to each child node.
-
Note how the character is not stored in the children but in the
-
edge of the parents:
-
[header iscompr=0][abc][a-ptr][b-ptr]c-ptr
-
if node is compressed (iscompr bit is 1) the node has 1 children.
-
In that case the ‘size’ bytes of the string stored immediately at
-
the start of the data section, represent a sequence of successive
-
nodes linked one after the other, for which only the last one in
-
the sequence is actually represented as a node, and pointed to by
-
the current compressed node.
-
[header iscompr=1][xyz]z-ptr
-
Both compressed and not compressed nodes can represent a key
-
with associated data in the radix tree at any level (not just terminal
-
nodes).
-
If the node has an associated key (iskey=1) and is not NULL
-
(isnull=0), then after the raxNode pointers poiting to the
-
children, an additional value pointer is present (as you can see
-
in the representation above as “value-ptr” field).
*/
unsigned char data[];
//data中包含:填充字段、当前节点包含的字符串以及子节点的指针、key对应的value指针
} raxNode; //raxNode代表Rax中的一个节点
为了实现Rax树的遍历,redis提供了 RaxStack、raxIterator 两种结构。
/* Stack data structure used by raxLowWalk() in order to, optionally, return
-
a list of parent nodes to the caller. The nodes do not have a “parent”
-
field for space concerns, so we use the auxiliary stack when needed. */
#define RAX_STACK_STATIC_ITEMS 32
typedef struct raxStack {
void *stack; / Points to static_items or an heap allocated array. */
size_t items, maxitems; /* Number of items contained and total space. */
/* Up to RAXSTACK_STACK_ITEMS items we avoid to allocate on the heap
- and use this static array of pointers instead. */
void *static_items[RAX_STACK_STATIC_ITEMS]; //这是一个数组,其中的每一个元素都指向一个存储路径
int oom; /* True if pushing into this stack failed for OOM at some point. */
} raxStack; //用于存储从根节点到当前节点的路径
typedef struct raxIterator {
int flags; //选择范围在下面
rax rt; / Radix tree we are iterating. */
unsigned char *key; //当前遍历到的key
void data; / Data associated to this key. */
size_t key_len; /* Current key length. */
size_t key_max; /* Max key len the current key buffer can hold. */
unsigned char key_static_string[RAX_ITER_STATIC_LEN]; //当key较大时,会从堆空间申请内存
raxNode node; / Current node. Only for unsafe iteration. */
raxStack stack; /* Stack used for unsafe iteration. */
raxNodeCallback node_cb; /* Optional node callback. Normally set to NULL. */
} raxIterator; //用于遍历Rax树中所有的key
#define RAX_ITER_JUST_SEEKED (1<<0)
/* Iterator was just seeked. Return current element for the first iteration and clear the flag. */
#define RAX_ITER_EOF (1<<1) /* End of iteration reached. */
#define RAX_ITER_SAFE (1<<2) /* Safe iterator, allows operations while iterating. But it is slower. */
这里可以稍微先瞄一眼,反正后面都是要去手写一下基数树的,先偷瞄一眼。
绕过重重包围,我们发现其实最终调用的是下面这个函数进行查找:
(不做解释,英文注释已经够清楚了)
/* Low level function that walks the tree looking for the string
-
‘s’ of ‘len’ bytes. The function returns the number of characters
-
of the key that was possible to process: if the returned integer
-
is the same as ‘len’, then it means that the node corresponding to the
-
string was found (however it may not be a key in case the node->iskey is
-
zero or if simply we stopped in the middle of a compressed node, so that
-
‘splitpos’ is non zero).
-
Otherwise if the returned integer is not the same as ‘len’, there was an
-
early stop during the tree walk because of a character mismatch.
-
The node where the search ended (because the full string was processed
-
or because there was an early stop) is returned by reference as
-
‘*stopnode’ if the passed pointer is not NULL. This node link in the
-
parent’s node is returned as ‘*plink’ if not NULL. Finally, if the
-
search stopped in a compressed node, ‘*splitpos’ returns the index
-
inside the compressed node where the search ended. This is useful to
-
know where to split the node for insertion.
-
Note that when we stop in the middle of a compressed node with
-
a perfect match, this function will return a length equal to the
-
‘len’ argument (all the key matched), and will return a *splitpos which is
-
always positive (that will represent the index of the character immediately
-
after the last match in the current compressed node).
-
When instead we stop at a compressed node and *splitpos is zero, it
-
means that the current node represents the key (that is, none of the
-
compressed node characters are needed to represent the key, just all
-
its parents nodes). */
static inline size_t raxLowWalk(rax *rax, unsigned char *s, size_t len, raxNode **stopnode, raxNode ***plink, int *splitpos, raxStack *ts) {
raxNode *h = rax->head;
raxNode **parentlink = &rax->head;
size_t i = 0; /* Position in the string. */
size_t j = 0; /* Position in the node children (or bytes if compressed).*/
while(h->size && i < len) {
debugnode(“Lookup current node”,h);
unsigned char *v = h->data;
if (h->iscompr) {
for (j = 0; j < h->size && i < len; j++, i++) {
if (v[j] != s[i]) break;
}
if (j != h->size) break;
} else {
/* Even when h->size is large, linear scan provides good
-
performances compared to other approaches that are in theory
-
more sounding, like performing a binary search. */
for (j = 0; j < h->size; j++) {
if (v[j] == s[i]) break;
}
if (j == h->size) break;
i++;
}
if (ts) raxStackPush(ts,h); /* Save stack of parent nodes. */
raxNode **children = raxNodeFirstChildPtr(h);
if (h->iscompr) j = 0; /* Compressed node only child is at index 0. */
memcpy(&h,children+j,sizeof(h));
parentlink = children+j;
j = 0; /* If the new node is non compressed and we do not
iterate again (since i == len) set the split
position to 0 to signal this node represents
the searched key. */
}
debugnode(“Lookup stop node is”,h);
if (stopnode) *stopnode = h;
if (plink) *plink = parentlink;
if (splitpos && h->iscompr) *splitpos = j;
return i;
}
我们发现真正插入的函数是这个(有点长哈)
int raxGenericInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old, int overwrite) {
size_t i;
int j = 0; /* Split position. If raxLowWalk() stops in a compressed
node, the index ‘j’ represents the char we stopped within the
compressed node, that is, the position where to split the
node for insertion. */
raxNode *h, **parentlink;
debugf(“### Insert %.*s with value %p\n”, (int)len, s, data);
i = raxLowWalk(rax,s,len,&h,&parentlink,&j,NULL);
/* If i == len we walked following the whole string. If we are not
-
in the middle of a compressed node, the string is either already
-
inserted or this middle node is currently not a key, but can represent
-
our key. We have just to reallocate the node and make space for the
-
data pointer. */
if (i == len && (!h->iscompr || j == 0 /* not in the middle if j is 0 */)) {
debugf(“### Insert: node representing key exists\n”);
/* Make space for the value pointer if needed. */
if (!h->iskey || (h->isnull && overwrite)) {
h = raxReallocForData(h,data);
if (h) memcpy(parentlink,&h,sizeof(h));
}
if (h == NULL) {
errno = ENOMEM;
return 0;
}
/* Update the existing key if there is already one. */
if (h->iskey) {
if (old) *old = raxGetData(h);
if (overwrite) raxSetData(h,data);
errno = 0;
return 0; /* Element already exists. */
}
/* Otherwise set the node as a key. Note that raxSetData()
- will set h->iskey. */
raxSetData(h,data);
rax->numele++;
return 1; /* Element inserted. */
}
/* If the node we stopped at is a compressed node, we need to
-
split it before to continue.
-
Splitting a compressed node have a few possible cases.
-
Imagine that the node ‘h’ we are currently at is a compressed
-
node contaning the string “ANNIBALE” (it means that it represents
-
nodes A -> N -> N -> I -> B -> A -> L -> E with the only child
-
pointer of this node pointing at the ‘E’ node, because remember that
-
we have characters at the edges of the graph, not inside the nodes
-
themselves.
-
In order to show a real case imagine our node to also point to
-
another compressed node, that finally points at the node without
-
children, representing ‘O’:
-
"ANNIBALE" -> "SCO" -> []
-
When inserting we may face the following cases. Note that all the cases
-
require the insertion of a non compressed node with exactly two
-
children, except for the last case which just requires splitting a
-
compressed node.
-
- Inserting “ANNIENTARE”
-
|B| -> "ALE" -> "SCO" -> []
-
"ANNI" -> |-|
-
|E| -> (... continue algo ...) "NTARE" -> []
-
- Inserting “ANNIBALI”
-
|E| -> "SCO" -> []
-
"ANNIBAL" -> |-|
-
|I| -> (... continue algo ...) []
-
- Inserting “AGO” (Like case 1, but set iscompr = 0 into original node)
-
|N| -> "NIBALE" -> "SCO" -> []
-
|A| -> |-|
-
|G| -> (... continue algo ...) |O| -> []
-
- Inserting “CIAO”
-
|A| -> "NNIBALE" -> "SCO" -> []
-
|-|
-
|C| -> (... continue algo ...) "IAO" -> []
-
- Inserting “ANNI”
-
"ANNI" -> "BALE" -> "SCO" -> []
-
The final algorithm for insertion covering all the above cases is as
-
follows.
-
============================= ALGO 1 =============================
-
For the above cases 1 to 4, that is, all cases where we stopped in
-
the middle of a compressed node for a character mismatch, do:
-
Let $SPLITPOS be the zero-based index at which, in the
-
compressed node array of characters, we found the mismatching
-
character. For example if the node contains “ANNIBALE” and we add
-
“ANNIENTARE” the $SPLITPOS is 4, that is, the index at which the
-
mismatching character is found.
-
- Save the current compressed node $NEXT pointer (the pointer to the
-
child element, that is always present in compressed nodes).
-
- Create “split node” having as child the non common letter
-
at the compressed node. The other non common letter (at the key)
-
will be added later as we continue the normal insertion algorithm
-
at step “6”.
-
3a. IF $SPLITPOS == 0:
-
Replace the old node with the split node, by copying the auxiliary
-
data if any. Fix parent's reference. Free old node eventually
-
(we still need its data for the next steps of the algorithm).
-
3b. IF $SPLITPOS != 0:
-
Trim the compressed node (reallocating it as well) in order to
-
contain $splitpos characters. Change chilid pointer in order to link
-
to the split node. If new compressed node len is just 1, set
-
iscompr to 0 (layout is the same). Fix parent's reference.
-
4a. IF the postfix len (the length of the remaining string of the
-
original compressed node after the split character) is non zero,
-
create a "postfix node". If the postfix node has just one character
-
set iscompr to 0, otherwise iscompr to 1. Set the postfix node
-
child pointer to $NEXT.
-
4b. IF the postfix len is zero, just use $NEXT as postfix pointer.
-
- Set child[0] of split node to postfix node.
-
- Set the split node as the current node, set current index at child[1]
-
and continue insertion algorithm as usually.
-
============================= ALGO 2 =============================
-
For case 5, that is, if we stopped in the middle of a compressed
-
node but no mismatch was found, do:
-
Let $SPLITPOS be the zero-based index at which, in the
-
compressed node array of characters, we stopped iterating because
-
there were no more keys character to match. So in the example of
-
the node “ANNIBALE”, addig the string “ANNI”, the $SPLITPOS is 4.
-
- Save the current compressed node $NEXT pointer (the pointer to the
-
child element, that is always present in compressed nodes).
-
- Create a “postfix node” containing all the characters from $SPLITPOS
-
to the end. Use $NEXT as the postfix node child pointer.
-
If the postfix node length is 1, set iscompr to 0.
-
Set the node as a key with the associated value of the new
-
inserted key.
-
- Trim the current node to contain the first $SPLITPOS characters.
-
As usually if the new node length is just 1, set iscompr to 0.
-
Take the iskey / associated value as it was in the orignal node.
-
Fix the parent’s reference.
-
- Set the postfix node as the only child pointer of the trimmed
-
node created at step 1.
*/
/* ------------------------- ALGORITHM 1 --------------------------- */
if (h->iscompr && i != len) {
debugf(“ALGO 1: Stopped at compressed node %.*s (%p)\n”,
h->size, h->data, (void*)h);
debugf(“Still to insert: %.*s\n”, (int)(len-i), s+i);
debugf(“Splitting at %d: ‘%c’\n”, j, ((char*)h->data)[j]);
debugf(“Other (key) letter is ‘%c’\n”, s[i]);
/* 1: Save next pointer. */
raxNode **childfield = raxNodeLastChildPtr(h);
raxNode *next;
memcpy(&next,childfield,sizeof(next));
debugf(“Next is %p\n”, (void*)next);
debugf(“iskey %d\n”, h->iskey);
if (h->iskey) {
debugf(“key value is %p\n”, raxGetData(h));
}
/* Set the length of the additional nodes we will need. */
size_t trimmedlen = j;
size_t postfixlen = h->size - j - 1;
int split_node_is_key = !trimmedlen && h->iskey && !h->isnull;
size_t nodesize;
/* 2: Create the split node. Also allocate the other nodes we’ll need
- ASAP, so that it will be simpler to handle OOM. */
raxNode *splitnode = raxNewNode(1, split_node_is_key);
raxNode *trimmed = NULL;
raxNode *postfix = NULL;
if (trimmedlen) {
nodesize = sizeof(raxNode)+trimmedlen+raxPadding(trimmedlen)+
sizeof(raxNode*);
if (h->iskey && !h->isnull) nodesize += sizeof(void*);
trimmed = rax_malloc(nodesize);
}
if (postfixlen) {
nodesize = sizeof(raxNode)+postfixlen+raxPadding(postfixlen)+
sizeof(raxNode*);
postfix = rax_malloc(nodesize);
}
/* OOM? Abort now that the tree is untouched. */
if (splitnode == NULL ||
(trimmedlen && trimmed == NULL) ||
(postfixlen && postfix == NULL))
{
rax_free(splitnode);
rax_free(trimmed);
rax_free(postfix);
errno = ENOMEM;
return 0;
}
splitnode->data[0] = h->data[j];
if (j == 0) {
/* 3a: Replace the old node with the split node. */
if (h->iskey) {
void *ndata = raxGetData(h);
raxSetData(splitnode,ndata);
}
memcpy(parentlink,&splitnode,sizeof(splitnode));
} else {
/* 3b: Trim the compressed node. */
trimmed->size = j;
memcpy(trimmed->data,h->data,j);
trimmed->iscompr = j > 1 ? 1 : 0;
trimmed->iskey = h->iskey;
trimmed->isnull = h->isnull;
if (h->iskey && !h->isnull) {
void *ndata = raxGetData(h);
raxSetData(trimmed,ndata);
}
raxNode **cp = raxNodeLastChildPtr(trimmed);
memcpy(cp,&splitnode,sizeof(splitnode));
memcpy(parentlink,&trimmed,sizeof(trimmed));
parentlink = cp; /* Set parentlink to splitnode parent. */
rax->numnodes++;
}
/* 4: Create the postfix node: what remains of the original
- compressed node after the split. */
if (postfixlen) {
/* 4a: create a postfix node. */
postfix->iskey = 0;
postfix->isnull = 0;
postfix->size = postfixlen;
postfix->iscompr = postfixlen > 1;
memcpy(postfix->data,h->data+j+1,postfixlen);
raxNode **cp = raxNodeLastChildPtr(postfix);
memcpy(cp,&next,sizeof(next));
rax->numnodes++;
} else {
/* 4b: just use next as postfix node. */
postfix = next;
}
/* 5: Set splitnode first child as the postfix node. */
raxNode **splitchild = raxNodeLastChildPtr(splitnode);
memcpy(splitchild,&postfix,sizeof(postfix));
/* 6. Continue insertion: this will cause the splitnode to
-
get a new child (the non common character at the currently
-
inserted key). */
rax_free(h);
h = splitnode;
} else if (h->iscompr && i == len) {
/* ------------------------- ALGORITHM 2 --------------------------- */
debugf(“ALGO 2: Stopped at compressed node %.*s (%p) j = %d\n”,
h->size, h->data, (void*)h, j);
/* Allocate postfix & trimmed nodes ASAP to fail for OOM gracefully. */
size_t postfixlen = h->size - j;
size_t nodesize = sizeof(raxNode)+postfixlen+raxPadding(postfixlen)+
sizeof(raxNode*);
if (data != NULL) nodesize += sizeof(void*);
raxNode *postfix = rax_malloc(nodesize);
nodesize = sizeof(raxNode)+j+raxPadding(j)+sizeof(raxNode*);
if (h->iskey && !h->isnull) nodesize += sizeof(void*);
raxNode *trimmed = rax_malloc(nodesize);
if (postfix == NULL || trimmed == NULL) {
rax_free(postfix);
rax_free(trimmed);
errno = ENOMEM;
return 0;
}
/* 1: Save next pointer. */
raxNode **childfield = raxNodeLastChildPtr(h);
raxNode *next;
memcpy(&next,childfield,sizeof(next));
/* 2: Create the postfix node. */
postfix->size = postfixlen;
postfix->iscompr = postfixlen > 1;
postfix->iskey = 1;
postfix->isnull = 0;
memcpy(postfix->data,h->data+j,postfixlen);
raxSetData(postfix,data);
raxNode **cp = raxNodeLastChildPtr(postfix);
memcpy(cp,&next,sizeof(next));
rax->numnodes++;
/* 3: Trim the compressed node. */
trimmed->size = j;
trimmed->iscompr = j > 1;
trimmed->iskey = 0;
trimmed->isnull = 0;
memcpy(trimmed->data,h->data,j);
memcpy(parentlink,&trimmed,sizeof(trimmed));
if (h->iskey) {
void *aux = raxGetData(h);
raxSetData(trimmed,aux);
}
/* Fix the trimmed node child pointer to point to
- the postfix node. */
cp = raxNodeLastChildPtr(trimmed);
memcpy(cp,&postfix,sizeof(postfix));
/* Finish! We don’t need to continue with the insertion
- algorithm for ALGO 2. The key is already inserted. */
rax->numele++;
rax_free(h);
return 1; /* Key inserted. */
}
/* We walked the radix tree as far as we could, but still there are left
- chars in our string. We need to insert the missing nodes. */
while(i < len) {
raxNode *child;
/* If this node is going to have a single child, and there
-
are other characters, so that that would result in a chain
-
of single-childed nodes, turn it into a compressed node. */
if (h->size == 0 && len-i > 1) {
debugf(“Inserting compressed node\n”);
size_t comprsize = len-i;
if (comprsize > RAX_NODE_MAX_SIZE)
comprsize = RAX_NODE_MAX_SIZE;
raxNode *newh = raxCompressNode(h,s+i,comprsize,&child);
if (newh == NULL) goto oom;
h = newh;
memcpy(parentlink,&h,sizeof(h));
parentlink = raxNodeLastChildPtr(h);
i += comprsize;
} else {
debugf(“Inserting normal node\n”);
raxNode **new_parentlink;
raxNode *newh = raxAddChild(h,s[i],&child,&new_parentlink);
if (newh == NULL) goto oom;
h = newh;
memcpy(parentlink,&h,sizeof(h));
parentlink = new_parentlink;
i++;
}
rax->numnodes++;
h = child;
}
raxNode *newh = raxReallocForData(h,data);
if (newh == NULL) goto oom;
h = newh;
if (!h->iskey) rax->numele++;
raxSetData(h,data);
memcpy(parentlink,&h,sizeof(h));
return 1; /* Element inserted. */
oom:
/* This code path handles out of memory after part of the sub-tree was
-
already modified. Set the node as a key, and then remove it. However we
-
do that only if the node is a terminal node, otherwise if the OOM
-
happened reallocating a node in the middle, we don’t need to free
-
anything. */
if (h->size == 0) {
h->isnull = 1;
h->iskey = 1;
rax->numele++; /* Compensate the next remove. */
assert(raxRemove(rax,s,i,NULL) != 0);
}
errno = ENOMEM;
return 0;
}
删除要注意,删除之后可能要压缩一下哦!!!
/* Remove the specified item. Returns 1 if the item was found and
- deleted, 0 otherwise. */
int raxRemove(rax *rax, unsigned char *s, size_t len, void **old) {
raxNode *h;
raxStack ts;
debugf(“### Delete: %.*s\n”, (int)len, s);
raxStackInit(&ts);
int splitpos = 0;
size_t i = raxLowWalk(rax,s,len,&h,NULL,&splitpos,&ts);
if (i != len || (h->iscompr && splitpos != 0) || !h->iskey) {
raxStackFree(&ts);
return 0;
}
if (old) *old = raxGetData(h);
h->iskey = 0;
rax->numele–;
/* If this node has no children, the deletion needs to reclaim the
-
no longer used nodes. This is an iterative process that needs to
-
walk the three upward, deleting all the nodes with just one child
-
that are not keys, until the head of the rax is reached or the first
-
node with more than one child is found. */
int trycompress = 0; /* Will be set to 1 if we should try to optimize the
tree resulting from the deletion. */
if (h->size == 0) {
debugf(“Key deleted in node without children. Cleanup needed.\n”);
raxNode *child = NULL;
while(h != rax->head) {
child = h;
debugf("Freeing child %p [%.s] key:%d\n", (void)child,
(int)child->size, (char*)child->data, child->iskey);
rax_free(child);
rax->numnodes–;
h = raxStackPop(&ts);
/* If this node has more then one child, or actually holds
- a key, stop here. */
if (h->iskey || (!h->iscompr && h->size != 1)) break;
}
if (child) {
debugf(“Unlinking child %p from parent %p\n”,
(void*)child, (void*)h);
raxNode *new = raxRemoveChild(h,child);
if (new != h) {
raxNode *parent = raxStackPeek(&ts);
raxNode **parentlink;
if (parent == NULL) {
parentlink = &rax->head;
} else {
parentlink = raxFindParentLink(parent,h);
}
memcpy(parentlink,&new,sizeof(new));
}
/* If after the removal the node has just a single child
- and is not a key, we need to try to compress it. */
if (new->size == 1 && new->iskey == 0) {
trycompress = 1;
h = new;
}
}
} else if (h->size == 1) {
/* If the node had just one child, after the removal of the key
- further compression with adjacent nodes is pontentially possible. */
trycompress = 1;
}
/* Don’t try node compression if our nodes pointers stack is not
- complete because of OOM while executing raxLowWalk() */
if (trycompress && ts.oom) trycompress = 0;
面试结束复盘查漏补缺
每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。
以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~
重要的事说三遍,关注+关注+关注!
更多笔记分享
h->size != 1)) break;
}
if (child) {
debugf(“Unlinking child %p from parent %p\n”,
(void*)child, (void*)h);
raxNode *new = raxRemoveChild(h,child);
if (new != h) {
raxNode *parent = raxStackPeek(&ts);
raxNode **parentlink;
if (parent == NULL) {
parentlink = &rax->head;
} else {
parentlink = raxFindParentLink(parent,h);
}
memcpy(parentlink,&new,sizeof(new));
}
/* If after the removal the node has just a single child
- and is not a key, we need to try to compress it. */
if (new->size == 1 && new->iskey == 0) {
trycompress = 1;
h = new;
}
}
} else if (h->size == 1) {
/* If the node had just one child, after the removal of the key
- further compression with adjacent nodes is pontentially possible. */
trycompress = 1;
}
/* Don’t try node compression if our nodes pointers stack is not
- complete because of OOM while executing raxLowWalk() */
if (trycompress && ts.oom) trycompress = 0;
面试结束复盘查漏补缺
每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。
以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~
重要的事说三遍,关注+关注+关注!
[外链图片转存中…(img-6vjp8pwo-1714518695697)]
[外链图片转存中…(img-ItoVYgGN-1714518695698)]
更多笔记分享
[外链图片转存中…(img-ZP3HeEBz-1714518695698)]