数组与链表

本文探讨了数组和链表两种数据结构的特点。数组支持随机访问但需要连续内存,链表则是零散内存块。内容涉及在数组中插入数据、缓存管理、链表节点的删除和特定条件下的插入操作,以及代码在不同链表长度情况下的正确性检查。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数组
一、基本概念
1、什么数组?
数组是一种 线性表的数据结构。它用一组 连续的内存空间,来存储一组具有 相同类型的数据。
2、数组是如何实现下标随机访问数组元素的?
我们拿一个长度为10的数组来举例,在下面的图中,计算机给数组分配了一块连续的内存空间,其中内存的首地址为base_address=1000。
每次获取数据的时候,计算机都会根据下面的寻址公式计算出元素的存储的内存地址
a[i]_address = base_address + i * data_type_size

3、数组和链表的区别是什么?
  • 数组支持随机访问,链表不可以
  • 数组需要连续内存空间,链表是一组零散的内存块。
4、数组的插入操作
(1)假设数组的长度为n,现在,如果我们需要将一个数据插入到数组中的第K个位置,为了把第K个位置腾出来,给新来的数据,我们需要将第k~n的位置往后挪。
如果在数组的末尾插入元素,那就不需要移动数据,此时的时间复杂度为O(1),而如果在数组的第一个位置插入元素,那么就要移动后面的n个元素,此时的时间复杂度为o(n),因为在任何位置插入元素的概率都是一样的,所以平均时间复杂度为O((1+2+3+4+...+n)/n) = O(n+1/2) = O(n)
但是,如果我们的数组不要求有序的话,那么我们插入的时候只需要将第k个位置的元素放到最后,然后在k处插入我们的元素。此时时间复杂度为O(1)
5、数组的删除操作
如果要求数组是有序的话,那么时间复杂度和插入一样。
下面来说说数组是无序的情况下。
如果无序的时候,为避免元素移动,我们可以只将要被删除的元素删除,而不去搬移数据(比如js数组,可以置为null),当数组没有更多空间存储了,我们再去真正的删除操作,并且移动数组。
6、需要时刻警惕数组的访问越界的问题。
7、二维数组的寻址公式
对于m*n的数组:
a[i]_address = base_address + (i*n+j)*byte

8、利用链表来实现LRU缓存策略(最近最少使用策略)(使用次数是有序的)
利用一个有序数组,当有数据需要缓存的时候遍历数组
  1. 如果数据已经存在数组中了,则将此数据插入到数组头部,并将头部至数据原来位置的数据往后移一个位置。
  2. 如果数据不存在数组中,
  • 如果此时缓存已满,则将数组最后一个元素删除掉,把数组中所有数据往后移动,把数据插入到数组头部。
  • 如果此时缓存未满,则将数据插入到头部,然后所有数据往后移
链表
1、链表与数组的区别
首先从底层的存储结构来看
数组需要的是一块连续的内存空间来进行存储,如果计算机没有连续的足够大小的存储空间就会申请失败。 而链表并不需要连续的内存空间,它通过指针,将一组零散的内存块串联起来。
2、链表的结构
链表中最常见的三种结构分别是单链表,双链表,循环链表。
(1)单链表
链表通过指针将一组零散的内存块串联起来,这里的内存块就是链表的结点,为了将所有的链表串联起来,每个链表的结点除了存储数据意外,还需要记录链上的下一个结点的地址,称为后继指针next
单链表有两个很重要的结点,头结点和尾结点,一般链表指向的变量的就是头结点的地址,它记录着链表的基地址。而尾结点的next指向的是一个null空地址,代表链表已经没有后续结点了。
链表的插入和删除都比较简单,因为它不用搬移后面的结点,但是找到需要插入位置的前继结点,需要根据指针一个一个结点依次遍历,直到找到相应的结点,所以它的插入和删除时间复杂度都是O(n)。
(2)循环链表
循环链表就是在单链表的基础上,尾结点的next不再指向null,而是指向头结点。
(3)双向链表
双向链表是在单链表的基础上,每个结点多了一个前继指针prev,指向前一个结点。
双向链表比单链表多了一个存储空间,为什么我们还要用它呢?
虽然插入操作中,双向链表跟单链表时一样的时间复杂度,但是在删除操作中,双向链表还是比单链表有优势的:
从链表中删除一个数据无非就两种情况:
  • 删除结点中指定给定值的结点
  • 删除指定指针指向的结点
第一种情况,两种链表的操作都差不多,主要是第二种,
删除一个结点需要它的前继结点,因为需要修改它next指针指向的结点,如果是单链表,则还是需要遍历链表,判断结点的next是否指向特定的结点。但是双向链表,拥有prev,只要修改prev指向结点的next和next结点的prev就可以了。此时单链表的时间复杂度是O(n),而双向链表的时间复杂度是O(1)。
双向链表主要的思想就是用空间换时间,当内存足够的时候,为了加快代码的执行速度,那么就可以选择空间复杂度相对较高,但是时间复杂度相对较低的算法或数据结构,否则相反。
缓存实际上就是利用了空间换时间的设计思想。
3、利用链表来实现LRU缓存策略(最近最少使用策略)
利用一个有序的单链表,越靠近链表尾部的就是越早访问的,每次有新的数据访问的时候,就遍历一次链表,
  1. 如果此数据之前已经存在链表中了,那么就把这个结点在原来的位置删除并移到链表头部。
  2. 如果此前没有存在链表中:
  • 如果缓存未满,则将其插入链表头部。
  • 如果缓存已满,则将链表尾部的结点删除,将数据插入到链表头部。
链表与数组性能
链表代码书写技巧
1、一定要警惕指针丢失和内存泄漏!比如留意指针的指向问题,删除的时候要对删除结点释放内存空间。
2、针对链表的插入、删除操作,需要对 插入第一个结点和删除最后一个结点的情况进行特殊处理。为了解决这个问题,链表可以加一个头结点,head指针总是指向这个结点,因此这样的链表叫做带头链表。
3、重点留意边界条件处理。
  • 如果链表 为空,代码是否能正常工作
  • 如果链表只包含 一个结点的时候,代码是否能正常工作
  • 如果链表只包含 两个结点的时候,代码是否能正常工作
  • 代码逻辑在处理 头结点和尾结点的时候,是否能正常工作。
4、用js实现链表
(1)单链表
//初始化结点
class Node{
    constructor(key){
        this.next = null
        this.key = key
    }
}
//初始化单链表
class List{
    constructor(){
        this.head = null
        this.length = 0
    }


    //创建结点
     static createNode(key){
        return new  Node(key)
     }


     //往头部插入数据
     insert(node){
        if (this.head){
            //此时head有指向的结点
            node.next = this.head
        } else {
            node.next = null
        }
        this.head = node
        this.length++
     }
     //删除结点
     delete(node){
        //删除的结点时头结点指向的结点
         //删除的结点是尾结点
         //删除的结点时列表中间的结点
         if (node == this.head){
             this.head = node.next
             this.length --;
             return;
         }
         let prev = this.head
         while (prev.next!=node){
             prev = prev.next
         }

         if (node.next == null){
             prev.next = null
         }
         if (node.next){
             prev.next = node.next
         }
         this.length --;
     }
}

(2)双向链表
//初始化结点
class Node{
    constructor(key){
        this.next = null
        this.prev = null
        this.key = key
    }
}

//初始化链表
class List{
    constructor(){
        this.head = null
        this.length = 0
    }

    //创建结点
     static createNode(key){
        return new  Node(key)
     }

     //往头部插入数据
     insert(node){
         node.next = this.head
         node.prev = null
        if (this.head){
            //此时head有指向的结点
            this.head.prev = node
        } else {
            node.next = null
        }
        this.head = node
        this.length++
     }
     //删除结点
     delete(node){
        //删除的结点时头结点指向的结点
         //删除的结点是尾结点
         //删除的结点时列表中间的结点
         if (this.head == node){
             node.next.prev = null
             this.head = node.next
             this.length --
             return
         }

         if (node.next){
             node.prev.next = node.next
             node.next.prev = node.prev
         }else {
             //最后一个结点
             node.prev.next = null
         }
     }
}

5、常见链表操作
(1)单链表反转
 
var reverseList = function(head) {
    //如果空链表或者链表只有一个结点就返回原来的链表
    if (head == null || head.next == null) {
        return head
    }
    //遍历链表,将当前结点与上一个结点进行交换,prev用来存储当前结点
   let prev = null,cur = head
  while (cur){
      let temp = cur.next
      cur.next = prev
      prev = cur
      cur = temp
  }
    return prev


};

算法题练习
字符回文数判断
回文判断(函数)
编写一个函数int isPalindrome(char s[]),判断参数表示的字符串是否是回文,如果是返回1,否则返回0。在主函数中调用它,判断输入的字符串是否是回文,如果是,输出“yes”,如果不是,输出”No”。
回文的定义:“回文数”就是正读倒读都一样的字符串
输入
测试数据的个数 t
第一个字符串
第二个字符串
…….
输出
如果是,输出“yes”,如果不是,输出”No”
样例输入
3
abba
abcba
ab
样例输出
Yes
Yes
No
function huiwen(str) {
     if(str<0){
       return false
     }
     str +=''
    let length = str.length
    let t = length/2
    let flag = true
    for (let i=0;i<t;i++){
        console.log(str[i],str[length-i-1])
        if (str[i] != str[length-i-1]){
            flag = false
            break
        }
    }
    if (flag){
        console.log(str,'yes')
    } else {
        console.log(str,'no')
    }
}
huiwen(10)
huiwen('abcba')
huiwen('ab')

空间复杂度O(1), 空间复杂度看额外的内存消耗,而不是看数组本身存储需要多少空间!!
时间复杂度O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值