一个有趣的 C 链表习惯用法

发布于 2024-07-10 03:30:06 字数 958 浏览 5 评论 0原文

我参加了一个 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 字段,该字段位于我们要放入新记录的点之前。函数的最后一行要么更新 head列表的指针(如果插入发生在开头)或上一条记录的下一个字段,这非常酷。

有几个问题:

  • 这个习语有名字或者在任何文献中提到过吗?

  • C 语言中还有其他类似的吗?

我以为我很了解 C,并且很好地理解了指针和间接,但是我花了一段时间才完全理解。

I was at an interview for a C position in which they presented me with an idiom that I haven't previously encountered. This is a trick that simplifies implementation of various algorithms involving linked lists and I'm wondering if anybody else has encountered this.

Say we have a linked list record defined so:

typedef struct _record
{
    char* value;
    struct _record* next;
} record;

We need a function that inserts a new record so that the entire list remains sorted with respect to the value's in the records. The following implementation is simpler than anything I would have used, albeit less readable.

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;
}

When the function is called, r points to the head pointer of the list. During the while loop, r is updated to point to the next field of the record that comes just before the point where we want to put the new record in. The last line of the function either updates the head pointer of the list (if the insertion happens at the beginning) or the next field of the previous record, which is quite cool.

A couple of questions:

  • Does this idiom have a name or is it mentioned in any literature?

  • Are there others like it in the C language?

I thought I knew C pretty well and had pointers and indirection pretty well figured out, but this one took me a while to fully understand.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

留一抹残留的笑 2024-07-17 03:30:06

我用类似的方式插入二叉树。 因为在迭代树时,通常会在指针变为 NULL 时停止(您跑出了树)。

因此,要插入,您有 3 个选项,

1:使用一个变量来跟踪迭代指针的前一个值。

2:当你要跟随的指针为 NULL 时停止,然后再跟随它,可以工作,但在我看来稍微不太优雅。

3:或更优雅的解决方案是简单地使用指向指针的指针,因此您可以执行:*it = new_node();,它会将其添加到NULL的位置code> 曾经在你的树中。

对于链表,虽然此代码运行良好,但我通常只使用双向链表,这使得在任何位置插入都很简单。

I've used similar to this to insert into a binary tree. Because when iterating the tree, you usually stop when your pointer becomes NULL (you ran off the tree).

So to insert, you have 3 options,

1: use a variable which tracks the previous value of your iterating pointer.

2: stop when the pointer you would follow is NULL before you follow it, works but slightly less elegant in my opinion.

3: or a more elegant solution is simply use a pointer to a pointer, so you can just do: *it = new_node(); and it'll add it where the NULL used to be in your tree.

For a linked list, while this code works nicely, I usually just use a doubly linked list which makes it trivial to insert at any location.

难理解 2024-07-17 03:30:06

我想说的成语是“那种给‘c’带来坏名声的代码”

  • 毫无根据地聪明
  • 毫无根据地紧凑
  • 对调用者产生令人惊讶的副作用
  • malloc 上没有错误处理
  • 只适用于美国英语字符串

I'd say the idiom is "the kind of code which gave 'c' a bad name"

  • Unwarrantedly clever
  • Unwarrantedly compact
  • Surprising side effects on caller
  • No error handling on malloc
  • Only works for US English strings
醉南桥 2024-07-17 03:30:06

我没有看到任何我称之为习语本身的东西。 它看起来像是在 C 中处理数据结构时的标准编码。

我唯一的抱怨是调用者指针 (*r) 被修改了。 根据该功能的使用,我预计这会产生意想不到的副作用。 除了消除意外的副作用之外,使用局部变量来扮演 *r 的角色还可以使代码更具可读性。

I don't see anything I'd call an idiom per se. It looks like standard coding for when you deal with datastructures in C.

My only complaint would be that the callers pointer (*r) is modified. Depending on the use of the function, I'd expect thats an unexpected side effect. Besides removing the unexpected side effect, using a local variable to play the role of *r would make the code more readable.

独夜无伴 2024-07-17 03:30:06

这里的习语是什么? 肯定不是链表的实现。
指针结构的用法?
紧凑的循环?

就我个人而言,我会使用指针返回值而不是对输入值进行操作。
因为看到这个输入数据类型会敲响警钟,这让我在将值传递给您的函数之前复制它。

What would be the idiom here? Surely not the implementation of a linked list.
The usage of a pointer to pointer construct?
The compact loop?

Personally I'd use a pointer return value instead of operating on an input value.
Because seeing this input data type would ring a bell, which makes me copy my value before handing it to your function.

对你而言 2024-07-17 03:30:06

这是众所周知的事情——双指针迭代(这是我给它起的名字,我不知道正式名称)。 目标是能够在单个链表中找到一个位置,然后在该位置之前插入(在该位置之后插入是微不足道的)。 简单地实现,这需要两个指针(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;
}

This is a well known thing - double pointer iteration (that's my name for it, I don't know the official name). The goal is to be able to locate a position in single linked list, and then insert before that position (inserting after it is trivial). Implemented naively, this requires two pointers (current and prev) and special code for beginning of the list (when prev == NULL).

The code the way I usually write it is something along the lines of

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;
}

Update:

I've forgot theother purpose for this trick - a more important one. It's used to delete elements from single linked lists:

// 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;
}
三月梨花 2024-07-17 03:30:06

我不知道这是否有一个名称,或者即使它是一些特殊的习语,但是,由于现在内存相对充足,我的链接列表(语言库不让它们可用)是一个特殊的变体,它大大简化了代码。

首先,它们始终是双向链接的,因为这使得处理和插入/删除操作的两个方向的遍历都更容易。

“空”列表实际上由两个节点组成:头节点和尾节点。 通过这样做,插入和删除不需要担心它们要删除的节点是头节点还是尾节点,他们可以假设它是中间节点。

在节点 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

这一切都使代码更容易理解,而无需进行所有特殊处理边缘情况,以几个内存节点为代价。

I don't know if this has a name or even if it's some special idiom but, since memory is relatively plentiful nowadays, my linked lists (where the language libraries don't make them available) are a special variant which simplifies the code greatly.

For a start, they're always doubly-linked since this makes traversal in both directions easier, for both processing and insert/delete operations.

An 'empty' list actually consists of two nodes, the head and the tail. By doing this, inserts and deletes do not need to worry about whether the node they're deleting is the head or tail, they can just assume it's a middle node.

Insertion of a new node y before node x then become a simple:

x -> prev -> next = y
y -> next = x
y -> prev = x -> prev
x -> prev = y

Deletion of node x is a simple:

x -> prev -> next = x -> next
x -> next -> prev = x -> prev
free x

Traversal is adjusted to ignore the extraneous head and tail:

n = head -> next
while n != tail
    process n
    n = n -> next

This all serves to make the code a lot easier to understand without all the special handling of the edge cases, at the cost of a couple of nodes of memory.

如梦初醒的夏天 2024-07-17 03:30:06

最好不要将新节点的值作为输入/输出参数返回,而是将其作为函数的返回值。 这简化了调用代码和函数内部的代码(您可以摆脱所有那些丑陋的双重间接寻址)。

record* insert_sorted(const record* head, const char* value)

顺便说一句,您缺少对 malloc/strdup 失败案例的错误处理。

Instead of returning the value of the new node as an in/out parameter, you are better off having that be the return value of the function. This simplifies both the calling code, and the code inside the function (you can get rid of all those ugly double indirections).

record* insert_sorted(const record* head, const char* value)

You are missing error handling for the malloc/strdup failing case btw.

初熏 2024-07-17 03:30:06

这个习惯用法在《C 语言指针》第 12 章中给出。它用于将节点插入到没有链表头的链表中。

This idiom is given in the Chapter 12 of "Pointers on C".This is used to insert a node into a linked list without list head.

带上头具痛哭 2024-07-17 03:30:06

为了回答最初的问题,这被称为以指针为中心的方法,而不是朴素的以节点为中心的方法。 Rex Barzee 的《高级编程技术》第 3 章可在 amazon.com 获取包括一个更好的以指针为中心的方法的示例实现。

To answer the original question, this is known as a pointer centric approach instead of the naive node centric approach. Chapter 3 of "Advanced Programming Techniques" by Rex Barzee available at amazon.com includes a much better example implementation of the pointer centric approach.

时光无声 2024-07-17 03:30:06

我也想出了双指针的这种用法,我已经使用过它,但我不太喜欢它。 我想出的代码有这个内核来搜索某些对象并将它们从列表中删除:

Element** previous = &firstElement, *current;
while((current = *previous)) {
    if(shouldRemove(current)) {
        *previous = current->next;  //delete
    } else {
        previous = ¤t->next;  //point to next
    }
}

我不喜欢我的代码的原因是两个 if 子句之间的细微差别:语法几乎相同,但效果是完全不同的。 我不认为我们应该编写如此微妙的代码,但以不同的方式做会让代码变得非常冗长。 因此,无论哪种方式都不好 - 您可能会追求简洁或可读性,这是您的选择。

I have also come up with this use of a double pointer, I have used it, but I don't really like it. The code that I came up with has this kernel to search for certain objects and remove them from the list:

Element** previous = &firstElement, *current;
while((current = *previous)) {
    if(shouldRemove(current)) {
        *previous = current->next;  //delete
    } else {
        previous = ¤t->next;  //point to next
    }
}

The reason I don't like my code is the subtle difference between the two if clauses: the syntax is almost identical, but the effect is entirely different. I do not think, we should write code as subtle as this, but doing it differently makes the code really lengthy. So, either way is bad - you might go for brevity or for readability, it's your choice.

薄荷→糖丶微凉 2024-07-17 03:30:06

尽管有这些技巧,变量“r”的作用不是改变了吗? 呼叫者在呼叫后如何知道“*r”的含义? 或者执行后,列表的标题是什么?

我不敢相信这可以被举例说明(甚至在某些书中?!)。
我错过了什么吗?

如果您不返回任何指针(就像其他人建议的那样),那么我建议进行以下更改以保留输入的角色。

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;
    }

despite of the tricks, isn't the role of variable "r" changed? how does the caller tell what "*r" is for after calling? or after execution, what is the header of the list?

I could not believe this can be exemplified (even in some book?!).
Did I miss anything?

If you do not return any pointer (like the others suggested), then I would suggest following changes to keep the role of the input.

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;
}

actually, probably another better way to do it is using the "dummy head node", which links its next to the beginning of the list.

    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;
    }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文