iOS基础

iOS - 链表、数组区别及常见算法

2021-05-08  本文已影响0人  码代码的小马

链表和数组的区别

  • 数组需要一块连续的内存空间来存储,对内存要求比较高
  • 链表通过指针,将一组零散的内存块串联起来使用

链表类型

单链表、双向链表、循环链表、双向循环链表

链表和数组的优缺点

时间复杂度

  • 数组插入删除操作时间复杂度是O(n)
  • 链表插入删除操作时间复杂度是O(1)
    随机访问第k个元素
  • 数组:O(1)
  • 链表:O(n)

链表使用场景分析

  1. 删除操作
  1. 插入操作
    指定节点前面插入一个节点
  1. 查询
    对于一个有序链表,双向链表的按值查询的效率会比单链表高,可以记录上次查找的位置p,每次查询时,根据查找的值和p的大小关系,决定是往前还是往后查找,平均只需要找一半的数据

性能分析

数据简单易用, 使用连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,访问效率更高
链表在内存中不连续,对CPU缓存不友好

数组的确定是大小固定,一经声明就要占用整块连续的内存空间,如果声明数组过大,导致系统没有足够的连续内存空间分配给它,导致内存不足,声明数组过小,可能出现不够用的情况,只能再申请更大的内存空间,把原始数组拷贝过去,非常耗时。
链表本身没有限制,天然支持动态扩容

如果代码对内存的使用非常苛刻,数组更合适,链表中的每个节点都需要消耗额外的存储空间存储下一个节点的指针。对链表进行频繁的插入、删除操作还会导致内存频繁的申请和释放,容易造成内存碎片,如果是Java语言,可能导致GC

技巧

练习

  1. 从头到尾打印链表
    思路:
// 利用栈
void print(ListNode *head) {
   
   Stack *stack = Stack new();
   
   ListNode *node = head;
   while (node ! = NULL) {
       stack.push(node);
       node = node -> next;
   }
   
   while (!stack.empty()) {
       node = stack.top();
       print(" %d\t", node -> value);
       stack.pop();
   }
}

// 递归
void recursivePrint(List *head) {
   
   if (head == NULL) return;
   
   if (head -> next != NULL) {
       recursivePrint(head -> next);
   }
   
   printf("%d \t", head -> value);
}
  1. 删除单链表指定节点,时间复杂度O(1)
    思路:
  void deleteNode(ListNode *head, ListNode *deleteNode) {
    
    if (head == NULL || deleteNode == NULL) return ;
    
    
    if (deleteNode -> next != NULL) {
        // 删除的节点不是尾节点
        ListNode *temp = deleteNode -> next;
        deleteNode -> value = temp -> value;
        deleteNode -> next = temp -> next;
        
    } else if (head == deleteNode) {
        
        // 删除的是头节点
        head = NULL;
        deleteNode = NULL;
    } else {
        
        // 删除的是尾节点
        ListNode *node = head;
        while(node -> next != deleteNode) {
            // 找到前驱节点
            node = node -> next;
        }
        
        node -> next = NULL;
        deleteNode = NULL;
    }
    
}
  1. 链表中倒数第k个节点
    思路:
  2. 假设链表的长度是n,倒数第k个节点,如果从头计数的话,是n-k+1个节点,第一次遍历出节点的个数n,第二次遍历就能找到倒数的第k个节点
  3. 设置两个指针,第一个指针开始遍历向前走k-1步,第二个指针不动;从第k步开始,第二个指针也开始移动,两个节点始终保持k-1,当第一个指针走到尾节点时,第二个指针刚好指向倒数第k个节点
  ListNode* deleteKThNode(ListNode *head, int k) {
    
    if (!head || k == 0) return 0;
    
    ListNode *slow = head;
    ListNode *fast = head;
    
    for (int i = 0; i < k - 1; i ++) {
        
        if (fast -> next == NULL) return;  // 确保 K 的值小于链表长度
        
        fast = fast -> next;
    }
    
    while (fast -> next != NULL) {
        fast = fast -> next;
        slow = slow -> next;
    }
    
    return slow;
    
}

  1. 翻转链表
    参考:
    单链表反转总结篇
    一分钟教你链表反转
    单链表逆序掌握这两种思路就够了
  //三个指针
 struct ListNode* reverseList(struct ListNode* head) {

     if (head == NULL) return NULL;

     struct ListNode *pre = NULL;
     struct ListNode *next = NULL;
     struct ListNode *cur = head;

     while (cur != NULL) {
         next = cur -> next;
         cur -> next = pre;
         pre = cur;
         cur = next;
     }

     return pre;
 }

// 递归
struct ListNode* reverseList(struct ListNode* head) {
    
    if (head == NULL || head -> next == NULL) return head;
    
    struct ListNode *nextNode = head -> next;
    struct ListNode *reverse_node = reverseList(nextNode);
    nextNode -> next = head;
    head -> next = NULL;
    return reverse_node;
}

参考:

上一篇下一篇

猜你喜欢

热点阅读