数据结构之线性表

170 阅读4分钟

1-定义

线性表:零个或多个数据元素构成的有限序列,可以用序偶关系a1ai1aiai+1an(a1,…,ai-1,ai,ai+1,…,an)表示。

线性表的物理实现方式:数组与指针链。以数组方式实现时,称顺序表;以指针链实现时,称链表。

2-顺序表的C语言形式

需要说明的是:通常将一片连续的存储空间,称为内存数组;以内存数组来实现的线性表,叫顺序表;在许多编程语言中,会将顺序表称为数组。

操作系统分配内存时,需要用户指定待分配内存的大小,而数组申请内存空间时,需要指定两个信息:单个数据元素的内存大小,数组内的数据元素个数。数据元素在C语言中以固定大小的结构体表示。

// 数据元素
typeof struct { /* key-value pairs */} ElemType;

顺序表的表示:

#define DEFAULT_SIZE
typeof struct {
    ElemType    elements[DEFUALT_SIZE]   // 储存数据元素的内存数组    
    int         length;                  // 已存储的数据元素个数
    int         capacity;                // 可容纳的数据元素个数
}

// 或者

typeof struct {
    ElemType    *elem                    // 数组的基地址
    int         length;                  // 已存储的数据元素个数
    int         capacity;                // 可容纳的数据元素个数
}

3-链表的C语言形式

链表:每个节点内既存储了数据元素,又存储了后继节点的地址,节点之间地址不要求连续;

typeof struct Node {
    ElemType         data;       // 数据域,存储数据元素
    struct Node      *next       // 指针域,存储后继节点地址
    struct Node      *prev       // 指针域,存储前驱节点地址, 可选
} 

4-链表的JS实现

Javascript程序的内存是由运行时管理的,也没有指针数据类型,但JS对象是引用型,可以模拟指针操作。

数据结构的基本操作:查询、新增、删除,求链表的长度、遍历等;

操作基本可以分为两类:访问型与加工型操作。

一般,进行访问型和加工型操作时,需要考虑两个边界条件:

  • 数据结构为空;
  • 只有一个数据元素;

对于加工型的操作,还需考虑加工位置:

  • 头部新增;
  • 尾部新增;
  • 头部与尾部之间的某个位置进行新增;

在书写程序的过程中,我们理想的程序追求最短的时间复杂度与最小的内存分配,但在实践中,较少的时间复杂度需要分配更多的内存,而较小的内存分配需要更长的时间复杂度,两者往往不能兼得,需要根据实际情况权衡取舍。

在链表的操作中,求链表长度与尾部新增是很常见的操作,但是对于单链表,由于只保存了头节点的引用,使得求链表长度与尾部新增都需要遍历整个链表,时间复杂度为O(n),但如果在单链表中增加一个length属性来保存节点数,一个尾指针来保存最后一个节点的引用,那么使得这两种操作的时间复杂度降低为O(1)。虽然内存新增了一点点,但对于性能有极大的提升。当然,新增一个尾指针也会增加程序的复杂性,因为在进行新增或删除时,可能需要一直更新尾指针

class Node {
    constructor(data) {        
        this.data = data        
        this.next = null    
    }
    // 可根据实际需要,自定义
    compare(data) {        return Object.is(this.data, data)    }}
class LinkList {    
    constructor() {        
        this.head = null
        this.tail = null
        this.length = 0    
    }  
     
    // 头部新增   
    appendAtHead(data) {        
        const node = new Node(data)  
        if(!this.tail) this.tail = node            
        node.next = this.head
        this.head = node      
        this.length++
    }

   // 尾部新增
    appendAtTail(data) {
        const node = new Node(data)   
        if(!this.head) {
            this.head = this.tail = node
            this.length++
            return
        }     
        this.tail.next =  node 
        this.tail = node   
        this.length++   }

    // 头部删除
    removeAtHead() {
        if(!this.head) return null
        const data = this.head.data
        if(this.head === this.tail) {
            this.head = this.tail = null
        } else {
            this.head = this.head.next
        }
        this.length--

        return data
    }
    
    // 尾部删除
    removeAtTail() {
        if(!this.head) return null
        if(this.head === this.tail) {            
            return this.removeAtHead()        
        }        
        const data = this.tail.data
        let prev = null, current = this.head
        while(current !== this.tail) {
            prev = current
            current = current.next
        }
        prev.next = null
        this.tail = prev
        this.length--
        
        return data   
    }   

    remove(data) {
        let prev = null, current = this.head
        while(current) {
            if(current.compare(data)) {
                if(current === this.head) return this.removeAtHead()
                prev.next = current.next
                this.length--
                return current.data
            }
            prev = current
            current = current.next
        } 
        return null
    }

    contain(data) {
        let current = this.head
        while(current) {
           if(current.compare(data)) return true
           current = current.next
        }
        return false
    }

    find(data) {
        let current = this.head
        while(current) {
            if(current.compare(data)) return current            
            current = current.next
        }
        return null
    }

    // 使用JS语言特性,使可迭代
    [Symbol.iterator]() {
        let current = this.head
        return {
            next() { 
                let value, done
                if(current) { 
                    value = current.data;
                    done  = !!current.next 
                    current = current.next 
                } else { 
                    value = null     
                    done = false
                }
                return { value, done }
            },

            [Symbol.iterator]() { return this }
        }
    }
}

单链表中,头部新增、头部删除、尾部新增的时间复杂度为O(1),而尾部删除的时间复杂度为O(n);头部新增与头部删除,可以实现链栈;尾部新增与头部删除可以实现链队列;

数组中,头部新增与删除的时间复杂度为O(n),尾部新增与删除的时间复杂度为O(1);尾部增删可以实现顺序栈;

单链表中其他位置的增删,需要O(n)的查询,O(1)的增/删;数组中其他位置的增删,需要O(1)的查询,O(n)的移动;