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

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

链表和数组的区别


  • 数组需要一块连续的内存空间来存储,对内存要求比较高


  • 链表通过指针,将一组零散的内存块串联起来使用


链表类型


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


链表和数组的优缺点


时间复杂度


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


链表使用场景分析


1. 删除操作


  • 删除节点中"值等于某个给定值"的节点
    为了能找到节点,都需要从头遍历,尽管删除的时间复杂度是O()1,但是遍历查找的时间是主要的耗时点,对应的时间复杂度是O(n)
  • 删除特定的节点
    对于单链表来说,需要找到前驱节点,需要重头遍历,时间复杂度是O(n)
    对于双向链表,已经保存前驱节点,时间复杂度是O(1)


2. 插入操作


指定节点前面插入一个节点

  • 单链表,时间复杂度O(n)
  • 双向链表O(1)


3. 查询


对于一个有序链表,双向链表的按值查询的效率会比单链表高,可以记录上次查找的位置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)


思路:


  • 常规方法,遍历所有节点,找到指定节点的前驱节点p, 即p - >next = target, 然后p -> next = target -> next, 这种操作时间复杂度是O(n)
  • 特殊操作,把要删除的节点p的下一个节点q的值赋值给p,再把p的next指针指向q的下一个节点
    对于n-1个非尾节点,可以在O(1)时间内把下一个节点的内存复制到要删除的节点,并删除下一个节点,对于尾节点而言,仍然需要顺序查找O(n),总的平均时间复杂度是[(n-1)O(1) + O(n)]/n,结果还是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个节点


思路:


假设链表的长度是n,倒数第k个节点,如果从头计数的话,是n-k+1个节点,第一次遍历出节点的个数n,第二次遍历就能找到倒数的第k个节点


设置两个指针,第一个指针开始遍历向前走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;
}


相关文章
|
1月前
|
算法
Leetcode 初级算法 --- 数组篇
Leetcode 初级算法 --- 数组篇
38 0
|
1月前
|
存储 算法 搜索推荐
探索常见数据结构:数组、链表、栈、队列、树和图
探索常见数据结构:数组、链表、栈、队列、树和图
99 64
|
1月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
30 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
1月前
|
存储 缓存 C语言
C语言:链表和数组有什么区别
C语言中,链表和数组是两种常用的数据结构。数组是一种线性结构,元素在内存中连续存储,通过下标访问,适合随机访问且大小固定的情况。链表由一系列不连续的节点组成,每个节点存储数据和指向下一个节点的指针,适用于频繁插入和删除操作的场景,链表的大小可以动态变化。
|
1月前
|
存储 算法 定位技术
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
这篇文章主要介绍了稀疏数组和队列的概念、应用实例以及如何使用数组模拟队列和环形队列的实现方法。
20 0
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
|
1月前
|
算法
【链表】算法题(二) ----- 力扣/牛客
【链表】算法题(二) ----- 力扣/牛客
|
1月前
|
存储
一篇文章了解区分指针数组,数组指针,函数指针,链表。
一篇文章了解区分指针数组,数组指针,函数指针,链表。
17 0
|
1月前
|
算法
【链表】算法题(一) ----- 力扣 / 牛客
【链表】算法题(一) ----- 力扣 / 牛客
|
3月前
|
机器学习/深度学习 人工智能 算法
【语音识别算法】深度学习语音识别算法与传统语音识别算法的区别、对比及联系
深度学习语音识别算法与传统语音识别算法在理论基础、实现方式、性能表现等方面存在显著区别,同时也有一些联系。下面将从几个方面详细比较这两种方法,并给出应用实例和代码示例
44 4
|
3月前
|
存储 开发者 C#
WPF与邮件发送:教你如何在Windows Presentation Foundation应用中无缝集成电子邮件功能——从界面设计到代码实现,全面解析邮件发送的每一个细节密武器!
【8月更文挑战第31天】本文探讨了如何在Windows Presentation Foundation(WPF)应用中集成电子邮件发送功能,详细介绍了从创建WPF项目到设计用户界面的全过程,并通过具体示例代码展示了如何使用`System.Net.Mail`命名空间中的`SmtpClient`和`MailMessage`类来实现邮件发送逻辑。文章还强调了安全性和错误处理的重要性,提供了实用的异常捕获代码片段,旨在帮助WPF开发者更好地掌握邮件发送技术,提升应用程序的功能性与用户体验。
57 0