数据结构与算法 - 线性结构: 链表

139 阅读5分钟

数据结构与算法 - 线性结构: 单链表

单链表中的每个结点不仅包含值,还包含链接到下一个结点的引用字段。通过这种方式,单链表将所有结点按顺序组织起来。

下面是一个单链表的例子: image.png 蓝色箭头显示单个链表中的节点是如何组合到一起的.

结点结构:

// Definition for singly-linked list.
struct SinglyListNode {  
    int val;  
    struct SinglyListNode *next;  
};

操作

创建一个单链表

/**
* 创建链表结点
*/
struct SinglyListNode *createNode(int value) {  
    struct SinglyListNode *newNode = (struct SinglyListNode *) malloc(sizeof(struct SinglyListNode));  
    newNode->val = value;  
    newNode->next = NULL;  
    return newNode;  
}

/**  
* 创建单链表  
* @param arr  
* @param size  
* @return  
*/  
struct SinglyListNode *createLinkedList(int *arr, int size) {  
    if (size == 0) {  
        return NULL;  
    }  
    struct SinglyListNode *head = createNode(arr[0]);  
    struct SinglyListNode *curr = head;  
    for (int i = 1; i < size; i++) {  
        struct SinglyListNode *newNode = createNode(arr[i]);  
    curr->next = newNode;  
    curr = curr->next;  
    }  
    return head;  
}

添加操作 - 单链表

如果我们想在给定的结点 prev 之后添加新值,我们应该:

image.png

image.png

image.png 与数组不同,我们不需要将所有元素移动到插入元素之后。因此,您可以在 O(1) 时间复杂度中将新结点插入到链表中,这非常高效。

示例

image.png

让我们在第二个结点 6 之后插入一个新的值 9 。

我们将首先初始化一个值为 9 的新结点。然后将结点 9 链接到结点 15 。最后,将结点 6 链接到结点 9 。

插入之后,我们的链表将如下所示:

image.png

在开头添加结点

众所周知,我们使用头结点来代表整个列表。

因此,在列表开头添加新节点时更新头结点 head 至关重要。

初始化一个新结点 cur ; 将新结点链接到我们的原始头结点 head。 将 cur 指定为 head 。 例如,让我们在列表的开头添加一个新结点 9 。

  1. 我们初始化一个新结点 9 并将其链接到当前头结点 23

image.png

  1. 指定结点 9 为新的头结点

image.png

/**  
* 头插法  
* @param head  
* @param value  
*/  
void insertAtHead(struct SinglyListNode **head, int value) {  
    struct SinglyListNode *newNode = createNode(value);  
    newNode->next = *head;  
    *head = newNode;  
}  
  
/**  
* 尾插法  
* @param head  
* @param value  
*/  
void insertAtTail(struct SinglyListNode **head, int value) {  
    struct SinglyListNode *newNode = createNode(value);  
    if (*head == NULL) {  
        *head = newNode;  
    } else {  
        struct SinglyListNode *curr = *head;  
    while (curr->next != NULL) {  
        curr = curr->next;  
    }  
    curr->next = newNode;  
    }  
}  
  
/**  
* 随机插入  
* @param head  
* @param value  
* @param position  
*/  
void insertAtPosition(struct SinglyListNode **head, int value, int position) {  
    if (position < 0)  
        position = 0;  

    if (position == 0) {  
        insertAtHead(head, value);  
        return;  
    }  

    struct SinglyListNode *newNode = createNode(value);  
    struct SinglyListNode *curr = *head;  
    int currentPosition = 0;  

    while (curr != NULL && currentPosition < position - 1) {  
        curr = curr->next;  
        currentPosition++;  
    }  
  
    if (curr == NULL)  
        return;  
  
    newNode->next = curr->next;  
    curr->next = newNode;  
}

删除操作 - 单链表

如果我们想从单链表中删除现有结点 cur,可以分两步完成:

  1. 找到 cur 的上一个结点 prev 及其下一个结点 next

image.png 2. 接下来链接 prev 到 cur 的下一个节点 next 。

image.png

在我们的第一步中,我们需要找出 prev 和 next。使用 cur 的参考字段很容易找出 next,但是,我们必须从头结点遍历链表,以找出 prev,它的平均时间是 O(N),其中 N 是链表的长度。因此,删除结点的时间复杂度将是 O(N)。

空间复杂度为 O(1),因为我们只需要常量空间来存储指针。

示例

image.png 让我们尝试把结点 6从上面的单链表中删除。

  1. 从头遍历链表,直到我们找到前一个结点 prev,即结点 23

  2. 将 prev(结点 23)与 next(结点 15)链接

image.png

删除第一个结点

正如之前所提到的,我们使用头结点 head 来表示链表。我们的头是下面示例中的黑色结点 23。

image.png 如果想要删除第一个结点,我们可以简单地将下一个结点分配给 head。也就是说,删除之后我们的头将会是结点 6。

image.png

/**  
* 删除头结点  
* @param head  
*/  
void deleteHead(struct SinglyListNode **head) {  
    if (*head == NULL)  
        return;  

    struct SinglyListNode *temp = *head;  
    *head = (*head)->next;  
    free(temp);  
}  
  
/**  
* 删除尾结点  
* @param head  
*/  
void deleteTail(struct SinglyListNode **head) {  
    if (*head == NULL)  
        return;  

    if ((*head)->next == NULL) {  
        free(*head);  
        *head = NULL;  
        return;  
    }  

    struct SinglyListNode *curr = *head;  
    struct SinglyListNode *prev = NULL;  

    while (curr->next != NULL) {  
        prev = curr;  
        curr = curr->next;  
    }  

    prev->next = NULL;  
    free(curr);  
}  
  
/**  
* 删除中间结点  
* @param head  
* @param position  
*/  
void deleteMiddle(struct SinglyListNode **head, int position) {  
    if (*head == NULL || position <= 0)  
        return;  

    if (position == 1) {  
        deleteHead(head);  
        return;  
    }  

    struct SinglyListNode *curr = *head;  
    struct SinglyListNode *prev = NULL;  
    int currentPosition = 1;  

    while (curr != NULL && currentPosition < position) {  
        prev = curr;  
        curr = curr->next;  
        currentPosition++;  
    }  

    if (curr == NULL)  
        return;  

    prev->next = curr->next;  
    free(curr);  
}

数据结构与算法 - 线性结构: 双链表

双链表以类似的方式工作,但还有一个引用字段,称为“prev”字段。有了这个额外的字段,您就能够知道当前结点的前一个结点。

image.png 双链表结点结构

struct DoublyListNode {  
    int val;  
    struct DoublyListNode *prev;  
    struct DoublyListNode *next;  
};

操作

添加操作 - 双链表

如果我们想在现有的结点 prev 之后插入一个新的结点 cur,我们可以将此过程分为两个步骤:

  1. 链接 cur 与 prev 和 next,其中 next 是 prev 原始的下一个节点;

image.png 2. 用 cur 重新链接 prev 和 next

image.png 与单链表类似,添加操作的时间和空间复杂度都是 O(1)

/**  
* 头插法  
* @param head  
* @param value  
*/  
void insertAtHead(struct DoublyListNode **head, int value) {  
    struct DoublyListNode *newNode = createNode(value);  
    if (*head == NULL) {  
        *head = newNode;  
        return;  
    }  
    newNode->next = *head;  
    (*head)->prev = newNode;  
    *head = newNode;  
}  
  
/**  
* 尾插法  
* @param head  
* @param value  
*/  
void insertAtTail(struct DoublyListNode **head, int value) {  
    struct DoublyListNode *newNode = createNode(value);  
    if (*head == NULL) {  
        *head = newNode;  
        return;  
    }  
    struct DoublyListNode *curr = *head;  
    while (curr->next != NULL) {  
        curr = curr->next;  
    }  
    curr->next = newNode;  
    newNode->prev = curr;  
}  
  
/**  
* 随机插入  
* @param head  
* @param value  
* @param position  
*/  
void insertAtPosition(struct DoublyListNode **head, int value, int position) {  
    if (position <= 0) {  
        insertAtHead(head, value);  
        return;  
    }  
    struct DoublyListNode *newNode = createNode(value);  
    struct DoublyListNode *curr = *head;  
    int currentPosition = 1;  
    while (curr != NULL && currentPosition < position) {  
        curr = curr->next;  
        currentPosition++;  
    }  
    if (curr == NULL) {  
        insertAtTail(head, value);  
        return;  
    }  
    newNode->next = curr;  
    newNode->prev = curr->prev;  
    curr->prev->next = newNode;  
    curr->prev = newNode;  
}

删除操作 - 双链表

如果我们想从双链表中删除一个现有的结点 cur,我们可以简单地将它的前一个结点 prev 与下一个结点 next 链接起来。

/**  
* 删除头节点  
* @param head  
*/  
void deleteHead(struct DoublyListNode **head) {  
    if (*head == NULL)  
        return;  
    struct DoublyListNode *temp = *head;  
    *head = (*head)->next;  
    if (*head != NULL)  
        (*head)->prev = NULL;  
    free(temp);  
}  
  
/**  
* 删除尾结点  
* @param head  
*/  
void deleteTail(struct DoublyListNode **head) {  
    if (*head == NULL)  
        return;  
    struct DoublyListNode *curr = *head;  
    while (curr->next != NULL) {  
        curr = curr->next;  
    }  
    if (curr->prev != NULL)  
        curr->prev->next = NULL;  
    else  
        *head = NULL;  
    free(curr);  
}  
  
/**  
* 随机删除结点  
* @param head  
* @param node  
*/  
void deleteNode(struct DoublyListNode **head, struct DoublyListNode *node) {  
    if (*head == NULL || node == NULL)  
        return;  
    if (node == *head) {  
        deleteHead(head);  
        return;  
    }  
    if (node->next != NULL)  
        node->next->prev = node->prev;  
    if (node->prev != NULL)  
        node->prev->next = node->next;
    free(node);  
}