《数据结构教程》2.3 线性链表

为了弥补和克服上一节所述顺序存储结构所带来的的不足,这一节讨论线性表的另外一种存储结构——链式存储结构。链式存储结构不要求逻辑上相邻的数据元素在物理位置上也相邻,经过 指针 来映射数据元素之间的逻辑关系。node

使用连式存储结构时,每一个数据元素除了存储自身的数据信息外,还须要存储一个指示其直接后继元素位置的信息,这两部分组成一个 链结点面试

当链表中每个链结点除了数据域外只设置一个指针域时,称这样的链表为 线性链表单链表算法

C 语言实现

链结点

一个链结点定义以下:函数

typedef struct node {
  ElemType data;
  struct node *link;
} LNode, *LinkList;
复制代码

基本操做

线性链表的基本操做都比较简单,挑几个重点说明以下。ui

建立链表

书中没有指定每一个数据元素的来源,这里假设是由用户输入的,使用 scanf 函数。spa

/** * 1. 创建一个线性链表 */
LinkList create(int n) {
  LinkList list = NULL, rear = NULL;

  for (int i = 0; i < n; i++) {
    int val;
    scanf("%d", &val);

    LNode *node = malloc(sizeof(LNode));
    node -> data = val;
    node -> link = NULL;

    if (list == NULL) {
      list = node;
    } else {
      rear -> link = node;
    }

    rear = node;
  }

  return list;
}
复制代码

须要注意一下若是使用的是判断 rear == NULL,那么 rear 在声明的时候必须赋初值为 NULL,不然 rear 初始化时是一个垃圾值,不会进入判断条件,使得下面调用 rear -> link 会抛出异常。指针

插入结点

插入分了三种,往头部插入、往尾部插入以及在某个结点后插入。后面两种比较简单,但注意第一种。C 语言是 按值传递,若是此处函数签名不是 LinkList 的指针(LinkList*),而直接使用 LinkList,那么在最后赋值时,list = newNode 是不会起做用的,由于修改的只是 形参的指针code

/** * 5. 在非空线性链表第一个链结点前插入一个 item * * 注意:签名必须是指针的指针 */
void insertLink1(LinkList *list, ElemType item) {
  LinkList newNode = malloc(sizeof(LNode));
  newNode -> data = item;
  newNode -> link = *list;
  *list = newNode;
}

/** * 6. 在非空线性链表的末尾插入一个 item */
void insertLink2(LinkList list, ElemType item) {
  LinkList rear = list;

  while (rear -> link != NULL) {
    rear = rear -> link;
  }

  LinkList newNode = malloc(sizeof(LNode));
  newNode -> data = item;
  newNode -> link = NULL;
  rear -> link = newNode;
}

/** * 7. 在线性链表指针 p 后面插入一个 item */
void insertLink3(LinkList list, LinkList q, ElemType item) {
  LinkList newNode = malloc(sizeof(LNode));
  newNode -> data = item;

  if (list == NULL) {
    newNode -> link = NULL;
    list = newNode;
  } else {
    newNode -> link = q -> link;
    q -> link = newNode;
  }
}
复制代码

经典算法题

线性链表的反转

leetcode 206. 反转链表cdn

线性链表的反转是一个比较经典的面试题,实际上实现起来也比较简单,主要须要理清思路。blog

实际上须要维护的指针有三个:

  • 当前遍历到的链结点(cur
  • 上一个链结点(prev
  • 上上个链结点(prevPrev

当每一步进行反转时,实际上反转的是将 prev 指向 prevPrev,由于 cur 指针的 link 是不能动的,不然你就找不到下一个链结点了

因此思路是(while 循环中的四行代码):

  1. 上上个结点(prevPrev)日后走
  2. 上个结点(prev)日后走
  3. 当前结点(cur)日后走
  4. 反转 prevprevPrev
/** * 13. 线性链表的反转 */
void invert(LinkList *list) {
  LinkList cur = *list, prev = NULL, prevPrev = NULL;

  while (cur != NULL) {
    prevPrev = prev;
    prev = cur;
    cur = cur -> link;
    prev -> link = prevPrev;
  }

  *list = prev;
}
复制代码

删除链表的倒数第 N 个节点

leetcode 19. 删除链表的倒数第N个节点

由于链表是单向的,没法直接从后往前寻找倒数第 N 个节点。要使时间复杂度为 O(n),即一次遍历实现,能够基于以下思路:

使用两个指针,其中一个指针比另外一个领先 N 个节点,而后两个节点同时日后移动,当前面的指针到达结尾时,后面的指针则到达了倒数第 N 个位置。

struct ListNode* removeNthFromEnd(struct ListNode* head, int n){
    struct ListNode* pioneer = head;
    struct ListNode* prev = head;
    
    for (int i = 0; i < n; i++) {
        pioneer = pioneer -> next;
    }
    
    if (pioneer == NULL) {
        prev = prev -> next;
        return prev;
    }
    
    while (pioneer -> next != NULL) {
        prev = prev -> next;
        pioneer = pioneer -> next;
    }
    
    prev -> next = prev -> next -> next;
    
    return head;
}
复制代码
相关文章
相关标签/搜索