02、数据结构与算法(数组与链表)

145 阅读14分钟

1、数组

1.1数组的概念

数组这种数据结构,是存储相同数据类型的一块连续的存储空间。解读一下这个定义的话,那就是:数组中的数据必须是相同类型的,数组中的数据必须是连续存储的。只有这样,数组才能实现根据下标快速地(时间复杂度是O(1))定位一个元素(快速的随机访问)
但是在Javascript这种语言中并不完全符合上面的定义,Javascript数组中的数据不一定是连续存储的,也不一定非得是相同类型,甚至数组可以是变长的。

1.2不同语言中数组的区别

1.2.1C/C++中数组的实现方式

C/C++中的数组,是标准的数据结构中的数组,也就是连续存储相同类型的数据的一块内存空间。在C/C++中,不管是基本类型数据,比如int、long、char,还是结构体、对象,在数组中都是连续存储的。

int arr[3];arr[0] = 0;arr[1] = 1;arr[2] = 2;arr[3] = 3;

数组arr中存储的是int基本类型的数据,对应的内存存储方式,如果用画图的方式表示出来的话,就是下面这样子。从图中可以看出,数据是存储在一片连续的内存空间中的。

c数组.png

上述的是数组存储基本类型数据的例子,以下是数组存储struct结构体的例子。

struct Dog {  char a;  char b;};
struct Dog arr[3];
arr[0].a = '0'
arr[0].b = '1'
arr[1].a = '2'
arr[1].b = '3';
arr[2].a = '4';
arr[2].b = '5';

如果我们把这个结构体数组,用画图的方式表示出来,就是下面这个样子。我们发现,结构体数组中的元素,也是存储在一片连续的内存空间中的。

c2数组.png

1.2.2Javascript数组的存储方式以及长度

因为本人是前端开发,所以本文章说讲Javascript的数组。javascript中的数组元素可以是任何数据类型的。而在计算机中不同的数据类型数据是放在不同存储区的。如果数组中存储的是相同类型的数据,那JavaScript就真的用数据结构中数组来实现。也就是说,会分配一块连续的内存空间来存储数据。如果数组中存储的是非相同类型的数据,那JavaScript就用类似散列表的结构来存储数据。也就是说,数据并不是连续存储在内存中的。但是我们又不能按照内存地址来访问数组元素,那样会造成编程过程中的灾难。所以为了解决这个问题,我们按照下标的方式来对数组元素进行标记。所以我们计算数组长度的时候只需要计算数组元素的个数即可。

数组.jpg

javascript给数组提供了一个.length属性来计算数组长度。

var nameArr = new Array('zhangsan', 'lisi', 'wangwu');//['zhangsan', 'lisi', 'wangwu']
console.log(nameArr.length) //3

1.3Javascript数组常用的方法

1.3.1 push:向数组末尾追加数据,返回当前数组的长度

var arr = [1,2,3,9]
console.log(arr.push(6),arr) //5 [1,2,3,9,5]

1.3.2 pop:删除数组最后一个元素,并返回删除的这个元素。

var arr = ['zhangsan','lisi','wangwu']
console.log(arr.pop(),arr) // wangwu [ 'zhangsan', 'lisi' ]

1.3.3 shift(): 在数组头部删除一个元素,并返回删除的这个元素。

var arr = ['zhangsan','lisi','wangwu']
console.log(arr.shift(),arr) //zhangsan [ 'lisi', 'wangwu' ]

1.3.4 unshift():在数组头部添加一个元素,并返回添加元素后新数组的长度。

var arr = ['zhangsan','lisi','wangwu']
console.log(arr.unshift('zhaoliu'),arr) //4 [ 'zhaoliu', 'zhangsan', 'lisi', 'wangwu' ]

1.3.5 isArray:判断是否为数组

var arr = []; 
console.log(Array.isArray(arr))  // true
 
var arr1 = new Array(); 
console.log(Array.isArray(arr1))  //true

var arr2 = {}; 
console.log(Array.isArray(arr2))  //true

1.3.6 toString:将数组以字符串的形式返回

var arr = ['zhangsan','lisi','wangwu']
console.log(arr.toString()) // zhangsan,lisi,wangwu

1.3.7 reverse():数组反转

var arr = ['zhangsan','lisi','wangwu']
console.log(arr.reverse())  //[ 'wangwu', 'lisi', 'zhangsan' ]

1.3.8 slice:数组截取

//slice方法作用是能够根据指定的【起始点】和【结束点】来对数组进行截取,并生成一个新数组。
//新数组的内容是从起始点下标开始的元素到结束点下标的元素,但是不包括结束点下标的元素本身。
	 
var arr = ['zhangsan', 'lisi', 'wangwu'];
console.log(arr.slice(1,2));//[ 'lisi' ]
 
//slice方法的参数可以是负值。-1代表最后一个元素,-3代表倒数第三个元素。
 
var arr2 = ['zhangsan','lisi','SomeOne'];
console.log(arr2.slice(-3,-1));    //[ 'zhangsan', 'lisi' ]

1.3.9 splice:数组截取,并且可以插入新的元素(改变原数组)

splice()  方法通过删除现有元素和/或添加新元素来更改一个数组的内容。

一般的格式是这样:

array.splice(start) 
array.splice(start, deleteCount) 
array.splice(start, deleteCount, item1, item2, ...)

其中start是修改开始的位置,deleteCount是从start开始删除多少内容,其余的参数如果有的话就表示在start这个位置插入对应的元素。

var arr = ['zhangsan','lisi','wamhwu','zhaoliu'];
let newArr = arr.splice(1,2) // 从下标1开始删除两个元素
console.log(newArr,arr)  // [ 'lisi', 'wamhwu' ] [ 'zhangsan', 'zhaoliu' ]

var arr1 = ['zhangsan','lisi','wamhwu','zhaoliu']
let newArr1 = arr1.splice(2,2,'lihua','wangfang','xiaofang') // 从下标2开始删除两个元素,并将'lihua','wangfang','xiaofang'添加到数组里面
console.log(newArr1,arr1) // [ 'wamhwu', 'zhaoliu' ] [ 'zhangsan', 'lisi', 'lihua', 'wangfang', 'xiaofang' ]

1.3.10+ indexOf:索引

indexOf方法能够从前到后检索数组,并返回元素在数组中的第一次出现的下标。如果没有索引到则返回-1。indexOf第二个参数表示从第几个元素开始索引,是可选参数。

var arr = ['zhangsan','lisi','wangwu','zhaoliu'];
console.log(arr.indexOf('wangwu'))    //2
console.log(arr.indexOf('lihua'))    //-1

1.3.11 lastIndexOf:反序索引

lastIndexOf方法能够从后向前检索数组,并返回元素在数组中的最后一次出现的下标。如果没有索引到则返回-1。lastIndexOf第二个参数表示从第几个元素开始索引,是可选参数。

var arr = ['zhangsan','lisi','wangwu','zhaoliu','lisi','zhaoliu','wangwu','lisi'];
console.log(arr.lastIndexOf('wangwu'))   //6
console.log(arr.lastIndexOf('lisi',5))   //4  从第五个元素从后往前找

//第二个参数可以是负值。如果是-1则表示从最后一个元素开始向前查找.
console.log(arr.lastIndexOf('lisi',-1))   //7
console.log(arr.lastIndexOf('lisi',-2))   //4  从倒数第二个开始查找

1.3.12 includes

出自 ES2016(即 ES7),includes 方法提供了一种便捷的方式,用于检查数组是否包含某个特定的元素。使用 includes 方法时,只需传递所需寻找的元素,它便会返回一个布尔值,即数组中是否存在该元素。includes 方法还接受第二个参数,用以指定搜索的起始索引。

var arr = ['zhangsan','lisi','wangwu','zhaoliu'];
console.log(arr.includes('wangwu'))   //true
console.log(arr.includes('lihua'))    //false
console.log(arr.includes('lisi',2))   //false   从下标为2的开始搜索

2、链表

2.1 单向列表

链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有的语言称为指针或连接)组成。类似于火车头,一节车厢载着乘客(数据),通过节点连接另一节车厢。

链表2.png

  • head属性指向链表的第一个节点;
  • 链表中的最后一个节点指向null;
  • 当链表中一个节点也没有的时候,head直接指向null; 3.png

数组(标准数组)存在的缺点:

  • 数组(标准数组)的创建通常需要申请一段连续的内存空间(一整块内存),并且大小是固定的。所以当原数组不能满足容量需求时,需要扩容(一般情况下是申请一个更大的数组,比如2倍,然后将原数组中的元素复制过去)。
  • 在数组的开头或中间位置插入数据的成本很高,需要进行大量元素的位移。

链表的优势:

  • 链表中的元素在内存中不必是连续的空间,可以充分利用计算机的内存,实现灵活的内存动态管理
  • 链表不必在创建时就确定大小,并且大小可以无限地延伸下去。
  • 链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组效率高很多。

链表的缺点:

  • 链表访问任何一个位置的元素时,都需要从头开始访问(无法跳过第一个元素访问任何一个元素)。
  • 无法通过下标值直接访问元素,需要从头开始一个个访问,直到找到对应的元素。
  • 虽然可以轻松地到达下一个节点,但是回到前一个节点是很难的。

链表中的常见操作:

1、append(element):向链表尾部添加一个新的项;
2、insert(position,element):向链表的特定位置插入一个新的项;
3、get(position):获取对应位置的元素;
4、indexOf(element):返回元素在链表中的索引。如果链表中没有该元素就返回-1;
5、update(position,element):修改某个位置的元素;
6、removeAt(position):从链表的特定位置移除一项;
7、remove(element):从链表中移除一项;
8、isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;
9、size():返回链表包含的元素个数,与数组的length属性类似;
10、toString():由于链表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值;

2.2封装单向链表类

创建单向链表类

先创建单向链表类Linklist,并添加基本属性,再实现单向链表的常用方法:

//内部类  节点类
class ListNode{
    val
    next
    constructor(val){
        this.val = val
        this.next = null
    }
}
//单项列表类
class LinkList{
    head
    length
    constructor(){
        this.head = null
        this.length = 0
    }
}

以下是对链表类的方法的讲解,对于部分比较容易理解的代码暂不做讲解。

append(element):向链表尾部添加一个新的项;

//内部类
class ListNode{
    val
    next
    constructor(val){
        this.val = val
        this.next = null
    }
}

class LinkList{
    head
    length
    constructor(){
        this.head = null
        this.length = 0
    }

    // append(element):向链表尾部添加一个新的项
    append(element){
        let newNode = new ListNode(element)
        if(this.length === 0){
            this.head = newNode
        }else{
            let current = this.head
            while(current.next){
                current = current.next
            }
            current.next = newNode
        }
        this.length += 1
    }
 }

代码详解:

  • 首先让current指向第一个节点:

链表1.png

  • 通过while循环使current指向最后一个节点,最后通过current.next = newNode,让最后一个节点指向新节点newNode:

链表2.png

insert(position,element):向链表的特定位置插入一个新的项;

//内部类
class ListNode{
    val
    next
    constructor(val){
        this.val = val
        this.next = null
    }
}
class LinkList{
    head
    length
    constructor(){
        this.head = null
        this.length = 0
    }
    // insert(position,element):向链表的特定位置插入一个新的项;<br>
    insert(position,element){
        if(position < 0 || position > this.length){
            return false //超出边界值返回false,说明插入失败
        }

        let newNode = new ListNode(element)
        if(position === 0 ){
            let temp = this.head
            this.head = newNode
            newNode.next = temp
        }else{
            let index = 0 
            let previous
            let current = this.head
            while(index < position){
                index++
                previous = current
                current = current.next
            }
            previous.next = newNode
            newNode.next = current
        }
        this.length += 1
    }
}

代码详解:
inset方法实现的过程:根据插入节点位置的不同可分为多种情况:

  • 情况1:position = 0

通过: newNode.next = this.head,建立连接1;

通过: this.head = newNode,建立连接2;(不能先建立连接2,否则this.head不再指向Node1)

链表3.png

  • 情况2:position > 0

首先定义两个变量previous和curent分别指向需要插入位置pos = X的前一个节点和后一个节点;

然后,通过:newNode.next = current,改变指向3;

最后,通过:previous.next = newNode,改变指向4;

链表4.png

  • 情况2的特殊情形:position = length

情况2也包含了pos = length的情况,该情况下current和newNode.next都指向null;建立连接3和连接4的方式与情况2相同。

链表5.png

get(position):获取对应位置的元素;

//内部类 节点类
class ListNode{
    val
    next
    constructor(val){
        this.val = val
        this.next = null
    }
}
//单项列表类
class LinkList{
    head
    length
    constructor(){
        this.head = null
        this.length = 0
    }
    // get(position):获取对应位置的元素
    get(position){
        if(position < 0 || position > this.length){
            return null
        }
        let current = this.head
        let index = 0
        while(index < position){
            index++
            current = current.next
        }
        return current.val
    }
}

代码详解:
get方法的实现过程:以获取position = 2为例,如下图所示:

  • 首先使current指向第一个节点,此时index=0;

get1.png

  • 通过while循环使current循环指向下一个节点,注意循环终止的条件index++ < position,即当index=position时停止循环,此时循环了1次,current指向第二个节点(Node2),最后通过current.data返回Node2节点的数据;

get2.png

indexOf(element):返回元素在链表中的索引。如果链表中没有该元素就返回-1;

//内部类 节点类
class ListNode{
    val
    next
    constructor(val){
        this.val = val
        this.next = null
    }
}
//单项列表类
class LinkList{
    head
    length
    constructor(){
        this.head = null
        this.length = 0
    }
    //indexOf(element):返回元素在链表中的索引。如果链表中没有该元素就返回-1;
    indexOf(element){
        let current = this.head
        let index = 0
        while(current){
            if(element === current.val){
                return index
            }
            index += 1
            current = current.next
        }

        return -1
    }
}

代码详解:
indexOf方法的实现过程:

  • 使用变量current记录当前指向的节点,使用变量index记录当前节点的索引值(注意index = node数-1):

indexof1.png

update(position,element):修改某个位置的元素;

//内部类 节点类
class ListNode{
    val
    next
    constructor(val){
        this.val = val
        this.next = null
    }
}
//单项列表类
class LinkList{
    head
    length
    constructor(){
        this.head = null
        this.length = 0
    }
    // update(position,element):修改某个位置的元素;
    update(position,element){
        if(position < 0 || position > this.length){
            return false
        }
        let newNode = new ListNode(element)
        if(position === 0){
            newNode.next = this.head.next
            this.head = newNode
        }else{
            let index = 0
            let current = this.head
            let previous 
            while(index < position){
                previous = current
                current = current.next
                index++
            }
            previous.next = newNode
            newNode.next = current.next
        }
        return true
    }

removeAt(position):从链表的特定位置移除一项;

//内部类 节点类
class ListNode{
    val
    next
    constructor(val){
        this.val = val
        this.next = null
    }
}
//单项列表类
class LinkList{
    head
    length
    constructor(){
        this.head = null
        this.length = 0
    }
    // removeAt(position):从链表的特定位置移除一项;<br>
    removeAt(position){
        if(this.length <= 0 || position > this.length || position < 0){
            return null
        }
        if(position === 0){
            let val = this.head.val
            this.head = this.head.next
            this.length -= 1
            return val
        }else{
            let index = 0
            let current = this.head
            let previous
            while(index < position){
                previous = current
                current = current.next
                index++
            }
            previous.next = current.next
            this.length -= 1
            return current.val
        }
       
    }
}

代码详解:
removeAt方法的实现过程:删除节点时存在多种情况:

  • 情况1:position = 0,即移除第一个节点(Node1)。

通过:this.head = this.head.next,改变指向1即可;

虽然Node1的next仍指向Node2,但是没有引用指向Node1,则Node1会被垃圾回收器自动回收,所以不用处理Node1指向Node2的引用next。

111.png

  • 情况2:positon > 0,比如pos = 2即移除第三个节点(Node3)。

注意: position = length时position后一个节点为null不能删除,因此position != length;

首先,定义两个变量previous和curent分别指向需要删除位置pos = x的前一个节点和后一个节点;

然后,通过:previous.next = current.next,改变指向1即可;

随后,没有引用指向Node3,Node3就会被自动回收,至此成功删除Node3 。

2222.png

单向列表完整代码

//内部类 节点类
class ListNode{
    val
    next
    constructor(val){
        this.val = val
        this.next = null
    }
}
//单项链表类
class LinkList{
    head
    length
    constructor(){
        this.head = null
        this.length = 0
    }

    // append(element):向链表尾部添加一个新的项
    append(element){
        let newNode = new ListNode(element)
        if(this.length === 0){
            this.head = newNode
        }else{
            let current = this.head
            while(current.next){
                current = current.next
            }
            current.next = newNode
        }
        this.length += 1
    }
    // insert(position,element):向链表的特定位置插入一个新的项;<br>
    insert(position,element){
        if(position < 0 || position > this.length){
            return false //超出边界值返回false,说明插入失败
        }

        let newNode = new ListNode(element)
        if(position === 0 ){
            let temp = this.head
            this.head = newNode
            newNode.next = temp
        }else{
            let index = 0 
            let previous
            let current = this.head
            while(index < position){
                index++
                previous = current
                current = current.next
            }
            previous.next = newNode
            newNode.next = current
        }
        this.length += 1
    }
    // get(position):获取对应位置的元素
    get(position){
        if(position < 0 || position > this.length){
            return null
        }
        let current = this.head
        let index = 0
        while(index < position){
            index++
            current = current.next
        }
        return current.val
    }
    //indexOf(element):返回元素在链表中的索引。如果链表中没有该元素就返回-1;
    indexOf(element){
        let current = this.head
        let index = 0
        while(current){
            if(element === current.val){
                return index
            }
            index += 1
            current = current.next
        }

        return -1
    }
    // update(position,element):修改某个位置的元素;
    update(position,element){
        if(position < 0 || position > this.length){
            return false
        }
        let newNode = new ListNode(element)
        if(position === 0){
            newNode.next = this.head.next
            this.head = newNode
        }else{
            let index = 0
            let current = this.head
            let previous 
            while(index < position){
                previous = current
                current = current.next
                index++
            }
            previous.next = newNode
            newNode.next = current.next
        }
        return true
    }
    // removeAt(position):从链表的特定位置移除一项;<br>
    removeAt(position){
        if(this.length <= 0 || position > this.length || position < 0){
            return null
        }
        if(position === 0){
            let val = this.head.val
            this.head = this.head.next
            this.length -= 1
            return val
        }else{
            let index = 0
            let current = this.head
            let previous
            while(index < position){
                previous = current
                current = current.next
                index++
            }
            previous.next = current.next
            this.length -= 1
            return current.val
        }
       
    }
    // remove(element):从链表中移除一项;
    remove(element){
        //1.获取element在列表中的位置
        let position = this.indexOf(element)
        //2.根据位置信息,删除结点
        return this.removeAt(position)
    }
    // isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;
    isEmpty(){
        return this.length === 0
    }
    // size():返回链表包含的元素个数,与数组的length属性类似
    size(){
        return this.length
    }
    //由于链表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值;
    toString(){
        let current = this.head
        let listStr = ''
        while(current){
            listStr = listStr + current.val + ''
            current = current.next
        }
        return listStr
    }
}

const linkList = new LinkList()

linkList.append('111')
linkList.append('222')
linkList.append('333')
linkList.insert(0,'444')
linkList.update(0,'999')
linkList.update(1,'777')
console.log(linkList.get(3),linkList.indexOf('3393'),linkList.removeAt(6),linkList.toString(),linkList.indexOf('333'),linkList.remove('333'),linkList.toString(),linkList.size())   // 333 -1 null 999777222333 3 333 999777222 3

本打算在本本篇章也将一下双向列表的,但因为时间问题只能放到下个篇章来讲了。