我正在接受一个C职位的采访,他们向我提出了一个我以前没有遇到的习语。这是一个简化涉及链表的各种算法的实现的技巧,我想知道是否有其他人遇到过这个。
假设我们定义了一个链表记录:
typedef struct _record
{
char* value;
struct _record* next;
} record;
我们需要一个插入新记录的函数,以便整个列表保持对记录中的值进行排序。以下实现比我使用的任何内容都简单,尽管可读性较差。
void insert_sorted(record** r, const char* value)
{
record* newrec = NULL;
while(*r && strcmp(value, (*r)->value) > 0)
r = &((*r)->next); /* move r to point to the next field of the record */
newrec = malloc(sizeof(record));
newrec->value = strdup(value);
newrec->next = *r;
*r = newrec;
}
调用该函数时,r指向列表的头指针。在while循环期间,r被更新为指向我们想要放入新记录的点之前的记录的next
字段。函数的最后一行或者更新头部指针。列表(如果插入发生在开头)或前一条记录的next
字段,这很酷。
几个问题:
这个成语是否有名称或在任何文献中都有提及?
C语言中还有其他人喜欢吗?
我以为我非常了解C并且指针和间接很好地想通了,但这个花了我一段时间才完全理解。
答案 0 :(得分:8)
我会说成语是“那种给'c'起个坏名字的代码”
答案 1 :(得分:6)
我使用类似的方法插入到二叉树中。因为在迭代树时,通常会在指针变为NULL
(你跑掉树)时停止。
所以要插入,你有3个选项,
1:使用一个跟踪迭代指针前一个值的变量。
2:在你跟随它之前你所遵循的指针是NULL时停止,但在我看来这种效果稍微不那么优雅。
3:或者更优雅的解决方案只是使用指向指针的指针,这样你就可以执行:*it = new_node();
并将它添加到NULL
曾经在树中的位置。
对于链接列表,虽然这段代码运行良好,但我通常只使用双向链表,这使得在任何位置插入都很简单。
答案 2 :(得分:4)
我没有看到任何我称之为成语的东西。当您在C中处理数据结构时,它看起来像是标准编码。
我唯一的抱怨是修改了调用者指针(* r)。根据功能的用途,我预计这会产生意想不到的副作用。除了消除意外的副作用外,使用局部变量扮演* r的角色会使代码更具可读性。
答案 3 :(得分:3)
这里有什么成语?肯定不是链表的实现。 使用指针构造指针? 紧凑的循环?
我个人使用指针返回值而不是对输入值进行操作。 因为看到这个输入数据类型会响铃,这使我在将它交给你的函数之前复制我的值。
答案 4 :(得分:3)
这是众所周知的事情 - 双指针迭代(这是我的名字,我不知道正式名称)。目标是能够在单个链表中找到一个位置,然后在该位置之前插入(在它之后插入是微不足道的)。天真地实现,这需要两个指针(current和prev)和列表开头的特殊代码(当prev == NULL时)。
我通常编写代码的方式与
类似void insertIntoSorted(Element *&head, Element *newOne)
{
Element **pp = &head;
Element *curr;
while ((curr = *pp) != NULL && less(curr, newOne)) {
pp = &(pp->next);
}
newOne->next = *pp;
*pp = newOne;
}
更新
我已经忘记了这个技巧的另一个目的 - 一个更重要的目的。它用于从单个链接列表中删除元素:
// returns deleted element or NULL when key not found
Element *deleteFromList(Element *&head, const ElementKey &key)
{
Element **pp = &head;
Element *curr;
while ((curr = *pp) != NULL && !keyMatches(curr, key)) {
pp = &(pp->next);
}
if (curr == NULL) return NULL;
*pp = (*pp)->next; // here is the actual delete
return curr;
}
答案 5 :(得分:2)
我不知道这是否有一个名字,或者它是否是一些特殊的习语,但是,由于现在内存相对丰富,我的链表(语言库不提供它们)是一种特殊的变体,它简化了代码很大。
首先,它们总是双重链接,因为这样可以更方便地进行处理和插入/删除操作的双向遍历。
'空'列表实际上由两个节点组成,即头部和尾部。通过这样做,插入和删除不需要担心他们正在删除的节点是头还是尾,他们可以假设它是一个中间节点。
在节点x之前插入新节点y然后变为简单:
x -> prev -> next = y
y -> next = x
y -> prev = x -> prev
x -> prev = y
删除节点x很简单:
x -> prev -> next = x -> next
x -> next -> prev = x -> prev
free x
调整遍历以忽略无关的头尾:
n = head -> next
while n != tail
process n
n = n -> next
这一切都可以使代码更容易理解而无需对边缘情况进行所有特殊处理,而代价是几个内存节点。
答案 6 :(得分:1)
不是将新节点的值作为输入/输出参数返回,而是最好将其作为函数的返回值。这简化了调用代码和函数内部的代码(你可以摆脱所有那些丑陋的双重间接)。
record* insert_sorted(const record* head, const char* value)
你错过了malloc / strdup失败案例btw的错误处理。
答案 7 :(得分:1)
回答原始问题,这被称为以指针为中心的方法,而不是以天真节点为中心的方法。 Rex Barzee在amazon.com提供的“高级编程技术”第3章包含了一个更好的以指针为中心的方法实现。
答案 8 :(得分:1)
这个成语在“C指针”的第12章中给出。这用于将节点插入到没有列表头的链表中。
答案 9 :(得分:0)
我也想到了这个双指针的用法,我已经用过了,但我并不喜欢它。我想出的代码有这个内核来搜索某些对象并从列表中删除它们:
Element** previous = &firstElement, *current;
while((current = *previous)) {
if(shouldRemove(current)) {
*previous = current->next; //delete
} else {
previous = ¤t->next; //point to next
}
}
我不喜欢我的代码的原因是两个if子句之间的细微差别:语法几乎相同,但效果完全不同。我不认为,我们应该像这样编写代码,但是以不同的方式执行代码会使代码变得非常冗长。所以,无论哪种方式都很糟糕 - 你可能只是为了简洁或可读性,这是你的选择。
答案 10 :(得分:0)
我无法相信这可以作为例证(甚至在一些书中?!)。 我错过了什么吗?
如果你没有返回任何指针(就像其他建议的那样),那么我建议你进行以下更改以保持输入的作用。
void insert_sorted(record** head, const char* value)
{
record** r = head;
bool isSameHead = false;
record* newrec = NULL;
while(*r && strcmp(value, (*r)->value) > 0) {
r = &((*r)->next); isSameHead = true; }
newrec = malloc(sizeof(record));
newrec->value = strdup(value);
newrec->next = *r;
*r = newrec;
if (!isSameHead) *head = newrec;
}
实际上,可能另一种更好的方法是使用“虚拟头节点”,它将其旁边的链接链接到列表的开头。
void insert_sorted(record** head, const char* value)
{
record dummyHead;
dummyHead.next = *head;
record* r = &dummyHead;
while(r->next) {
if(strcmp(value, r->next->value) < 0)
break;
r = r->next;}
newrec = malloc(sizeof(record));
newrec->value = strdup(value);
newrec->next = r->next;
r->next = newrec;
*head = dummyHead.next;
}