TypeScript算法专题 - [单链表3]
链表的合并
以及对TypeScript链表实现中的一些问题总结与改进


李俊才
CSDN:jcLee95
邮箱:291148484@163.com

专题目录:https://blog.csdn.net/qq_28550263/article/details/115718216

【导读】
通过前两篇博客:

分类 标题 地址
数据结构.链表.单链表 blog1 - 基于TypeScript语言的单链表实现(含遍历插入结点等) https://blog.csdn.net/qq_28550263/article/details/115718830
数据结构.链表.单链表 blog2 - 单链表节点的索引结点删除与链表反转 https://blog.csdn.net/qq_28550263/article/details/115825987

中的编程实践,我们实现了一个简单的单链表。然而在代码调试过程中以及代码实现中都有很多值得来讨论的方面。

本文中的第一个话题是关于代码调试的:
这部分的内容主要是释疑,帮助读者理解为什么在调试代码的过程中,使用console.log()输出的内容于期待的内容又很大的差异。

导航
1. 使用console.log()进行代码分析时的意外情况提示
2. 使用对对象的引用
3.对链表代码的调整
4. 两个链表的合并实现代码


1. 使用console.log()进行代码分析时的意外情况提示

对于有过其它编程语言的读者,不论是C语言中的printf()函数,还是Python语言中的print()函数,又或者是Java语言中的println()函数,不管是编译型语言还是脚本语言,他们的执行都很能反应串行执行的特点,也就是说,执行语句是从开始到结束逐条执行。

“变态”的JavaScript以及其超集TypeScript让我们体验了一把与常识不一样的的感觉——使用conole.log()对象输出到控制台时,输出的内容往往并不是在该条console.log()语句执行时输出对象的”快照“。
举个例子
在浏览器环境中使用以下脚本:

let a = {"hello":"world"}
console.log(a);              // 第一次使用console.log()打印`a`
a.hello = "世界";
console.log(a);              // 第二次使用console.log()打印`a`

打开浏览器的控制台,可以看到:

不可思议的事情发生了—— 我们明明在第一次打印变量a后才对a进行修改,而实际结果却是两次打印a的值相同!
初次见到这种情况的你可能会到处找代码中的各种错误,一顿猛如虎的操作过后却发现没有任何的收获。不要慌,这时得赶紧想到可能真的并不是你的错误。

事实上,这种情况一般经常出现在浏览器运行代码的环境中。其原因时,浏览器在运行这段代码时可能会认为需要把控制台I/O延迟到后台。在这种情况下,等到浏览器控制台输出对象内容时,后面的语句可能已经执行,因此才会看到这样的输出结果。并且我们并没有办法确定什么时候控制台I/O会延迟,甚至是否能够被观察到。

2. 使用对对象的引用

TypeScript(或JavaScript)没有像C语言中那样的指针类型,不过却可以对对象进行引用。

先看两段代码:
【code1】

let a = 1;
let b = a;
b = 2;
console.log("a=",a,"b=",b)

Out[]:

a= 1 b= 2

【code2】

class Test{num:number;constructor(){this.num = 1}
}
let obj1: Test = new Test();
let obj2 = obj1;
obj2.num = 6;
console.log("obj1=",obj1)

Out[]:

obj1= Test { num: 6 }

在【code1】中可以看到,对变量b的赋值操作并没有改变变量a
而在【code2】中对obj2的更改却同时更改了obj1!!——这是为什么呢?

首先必须要指出的是,JavaScript(包括TypeScript)中,并非一切皆对象!这一点上与彻底的面向对象编程语言JavaPython等有所不同。

在【code1】中当我们给变量a赋值1后,由于数值就不是JavaScript对象,因此let b = a不会使得变量b建立到一个对象的引用。正是因为如此,b = 2只是使变量b中存储的内容为数值2,并没有改变变量a

在【code2】中,我们定义了一个Test类。首先我们构造了一个实例,通过变量obj1引用到该实例。在赋值语句let obj2 = obj1;中,由于obj1是对对象的引用,因此传递到变量obj2的也是同一个引用,意味着变量obj2和变量obj2一样,其中包含了的是同一个变量的地址,这个地址就是构造新的Test类时的这个类实例的地址。这就是为什么我们执行obj2.num=6时,obj1.num也同样变成了6

3.对链表代码的调整

现在我们使用变量引用的方法,将前两篇博客中的单链表代码调整得到如下代码:

class Lnode{/*** 链表结点类,结点是构成链表的基础单元。* @class Lnode* @constructor* @param next {Lnode} 指向下一结点的指针* @param data {string | number} 结点的数据。*/next: Lnode | undefined | null;              // 结点类型(Lnode)的nest也是结点类型Lnode,undefined和null是任意类型的子类型也可以不写data: any;empty: boolean;                              // 描述结点是否为空,这个是配合初始化一个空结点使用的constructor(data?:string | number){if(data!=undefined){                     // 非空结点this.empty = false;                  // 空标识置为假this.data = data;this.next = null;}else{                                   // 空结点,即值构建结点对象但实际上不存在结点this.empty = true;                   // 空标识置为真this.data = null;this.next = null;}}merge(node: Lnode){/*** 将另一个结点(链)合并到本结点(或链)的尾部* @param node {Lnode} 需要插入链表尾部的新结点*/if(this.empty==true){                    // 空结点this.empty =false;this.data = node.data;}else{if(this.next == null){               // 没有链接下一个this.next = node;}else{                               // 有链接下一个this.next.merge(node);}}}
}class LinkList{/*** 单链表类* @class LinkList* @constructor* @param head {Lnode} 链表的头指针,其`next`用于挂载结点* @param length {number} 链表的长度*/head: Lnode | undefined| null;length: number;                       // 链表所容纳结点的个数constructor(datas?:any[]){/**初始化 */if(datas==undefined){          this.head = null;             // 初始化为null算作空链表,不计算长度this.length = 0;}else{for(let i=0; i<datas.length; i++){this.append(new Lnode(datas[i]))}this.length = datas.length;  // 指定一组数据初始化则初始后长度为这组数据元素的个数}}is_empty(){/*** 判断链表是否为空* 只需要判断头结点是否为空,若头结点为空则为空链表,否则不是。* @return {Boolean} true:链表为空; false:链表非空。*/if(this.head == null){return true;}else{return false;}}top():Lnode{/*** 获取链表头结点*/let node = this.head;node.next = null;return node;}clear(){/*** 清空链表,只要把头结点干废,整个链表直接就清空了*/this.head = null;this.length = 0;}append(node: Lnode){/*** 将新结点挂载到链表尾部* @param node {Lnode} 需要插入链表尾部的新结点*/if(this.head == null){                     // 没有头结点this.head = node;this.length = this.length + 1;}else{let pointer = this.head;               // 指向头节点while(pointer.next != null){pointer = pointer.next}pointer.next = node}}append_left(node: Lnode){/*** 将一个新结点插入到链表首部* @param node {Lnode} 要从左侧也就是链头插入的新结点*/if(this.head == null){this.head = node;                     // 若为空链表,直接将链表头设置为该结点this.length = this.length + 1;        // 增加结点长度}else{// 先将新结点的`next`指向原第一个结点node.next = this.head;// 再将`head`指向新的结点this.head = node;this.length = this.length + 1;        // 增加结点长度}}get(index: number):Lnode{/*** 获取索引号为`index`的结点。* 索引号是从数字`0`开始计算的* @param index {number} 索引号* @return node {Lnode} 索引得到地节点*/if(index<0){throw "ValueError: Index must be a positive integer!"}else if(index+1 > this.length){throw "ValueError: Index exceeds the maximum value!"}else{let pointer:Lnode = this.head;       // 从头结点开始for(let i=0; i<index;i++){pointer = pointer.next;          // 逐个指向下一个结点}pointer.next = null;                 // 斩断后继return pointer;}}get_data(index: number){/*** 索引节点地数据* @param index {number} 索引号* @return data {any} 链表中与索引号对应地节点地数据域内容*/return this.get(index).data}insert(index:number, node:Lnode):void{/*** 在链表的第`index`个节的位置挂载新的结点`node`,其中从结点为`index = 0`开始编号* 也就是说,新的结点索引号将为`index`,而这个结点将挂载到索引号为`index-1`的结点后面* * @param index {number} 新结点插入的索引号*/if(node!=null && node.next==null){node.next = null;};                    // 只允许插入单个结点,若有后继直接切断if(index==0){node.next = this.head;                             // 先收小弟this.head = node;                                  // 再上位}else{let pointer = this.head;                           // 从头结点开始if(index==0){this.append_left(node)}else{for(let i=0; i<index-1; i++){                  // 调整指针所指,即调整引用对象位置pointer = pointer.next;                    // 逐个指向下一个结点,直至`pointer`指向被索引结点的前一个结点}node.next = pointer.next;                      // 先收小弟pointer.next = node                                 // 再上位this.length = this.length + 1;                 // 更新结点长度}}}p_insert(pointer:Lnode):void{/*** 在指链表指针所指结点的后面插入一个新的结点* @param pointer {Lnode} 模拟指针,实际上也是一个结点。*/}toArray():Lnode[]{/*** 获取链表结点依次构成的数组* @returns elem {Lnode[]} 以此装载了被遍历到的所有结点(这里其中每个结点都是孤立、干掉next的)*/let elm: Lnode[] = [];                              // 准备好用于容纳所有的结点let pointer:Lnode = this.head;                      // 挂载操作一开始时指针指向头部结点、if(pointer==null){                                  // 空链表,不仅要返回一个空数组,还要抛出提示console.warn("Warning: It seems that you hava traversed an empty Linked List!");return elm;}else{                                              // 非空链表while(pointer.next != null){                    // 存在下一个结点时if(pointer == null){                        // 停止(拦截)条件:(直到)结点为`null`break;}else{                                      // 获取当前结点剔除链接关系的`孤立结点`挂载结点数组elm.push(new Lnode(pointer.data));}pointer = pointer.next;                     // 指向后继}elm.push(new Lnode(pointer.data));              // 最后一个元素return elm;}}replace(index:number, nodes:Lnode){/*** 将索引为`index`处的结点替换为新结点`nodes`* 其中`node`既可以是单个节点又可以是后继指针非空的一系列节点* @param index {number} 将要被替换掉的节点的索引* @param nodes {Lnode} 替换上去的新节点*/function count_len_nodes(node:Lnode):number{/*** 用于当传入为止个数的节点链而非单个节点时,* 计算传入新传入的节点链长度 */let ct:number = 0;let pt:Lnode = node;while(pt!= null){ct++;pt = pt.next;}return ct;}if(nodes==null){throw "ValueError: Replacing with null is prohibited."      // 如果允许替换为`null`将把后续结点全部干掉,这就相当于执行删除的功能。因此禁止替换为null。}else{if(index<0){throw "ValueError: Index must be a positive integer!"}else if(index+1 > this.length){throw "ValueError: Index exceeds the maximum value!"}else{let len_nodes:number = 0;                        // 新结点(或节点链)长度if(nodes.next == null){len_nodes = 1}            // 传入单个结点else{len_nodes = count_len_nodes(nodes)};        // 传入的结点为有后继的结点链let pointer: Lnode = this.head;                  // 指向头结点的指针(实际上是引用)if(index==0){nodes.next = this.head.next;                 // 先收小弟(接纳原先头节点的后继)this.head = nodes;                           // 再上位(替换到原先头节点的位置上)}else{for(let i=0; i<index-1; i++){pointer = pointer.next;                  // 指向链表中的下一个结点,直至指向被替换结点的前一个结点}nodes.merge(pointer.next)                    // 先收小弟,即新结点接纳被替换结点的后继pointer.next = nodes;                        // 再上位,即被替换结点的前一个结点的后继指向新结点}this.length = this.length - 1 +len_nodes         // 更新链表长度}}}drop(index: number);drop(index:[number,number]);drop(index){if(this.head==null){throw "EelEmptyListError: Unable to pop up node from an empty linked list.";}else{if(typeof(index)=="number"){/*** 删除索引号为`index`的某个结点及其之后的所有结点* @param index {number} 结点索引号,该索引号指示结点及其之后全部结点都将从链表中删除*/if(index<0){throw "ValueError: Index must be a positive integer!"}else if(index+1 > this.length){throw "ValueError: Index exceeds the maximum value!";}else{if(index==0){this.clear()}else{let pointer:Lnode = this.head;for(let i=0; i<index-1; i++){pointer = pointer.next;               // 指向下一个,直至指向索引结点的前一个};pointer.next = null;                      // 通过清空被索引结点的前一个结点的后继指针,将被索引系欸但及其后从链表删除}}}else if(typeof(index)=="object"){/*** 删除索引号从`a`(包含)到`b`(包含)的一段结点* 允许`a`和`b`任意一个索引值更大,即不区分区间起始大小。* @param index {number[]} 包含两个索引值范围的数组。该范围内的所有结点都将从链表中删除。*/let a:number = index[0];let b:number = index[1];if(a>b){                                   // 不论先前哪一个索引值更大,始终转换为a更小、b更大a = index[1];b = index[0];}if(a<0){throw "ValueError: Index must be a positive integer!"}else if(b+1 > this.length){throw "ValueError: Index exceeds the maximum value!"}else{let pointer:Lnode = this.head;         // 引用头节点作为指针for(let i=0; i<a-1; i++){              // 处理结点范围为:[0:a-1]pointer = pointer.next;            // 以`pointer`为指针,逐步指向下一个结点,直至指向索引a结点的前一个结点};for(let i=0; i<b-a+1; i++){            // 处理结点范围为:[a:b]pointer.next = pointer.next.next   // 以`pointer.next`为指针,逐步指向下一个结点,直到跳过中间`b-a+1`个结点};                                     // 到此为止,后面[b+1,]的结点也是自动挂着的,无需再处理}}else{throw "TypeMismatchError: Type of actual parameter not match any overloaded method!"}}}del(index:number){/*** 删除索引号为`index`的某个结点,保留其后继结点* @param index {number} 将被删除的结点的索引号*/if(index<0){throw "ValueError: Index must be a positive integer!"}else if(this.head==null){throw "EelEmptyListError: Unable to pop up node from an empty linked list.";}else if(index+1 > this.length){throw "ValueError: Index exceeds the maximum value!";}else{if(index==0){this.pop_left();}else{let pointer:Lnode = this.head;               // 创建指针(对象引用)for(let i=0; i<index-1; i++){pointer = pointer.next;                  // 指向下一个,直至指向被索引结点的前一个结点}pointer.next = pointer.next.next             // 跳过被索引结点以实现删除之}}}pop():Lnode{/*** 弹出最右边的一个结点并返回* @returns node {Lnode} 原链表最右侧的一个结点*/if(this.head==null){throw "PopEmptyListError: Unable to pop up node from an empty linked list.";}else if(this.length==1){let last:Lnode = this.head;this.head = null;return last}else{let pointer = this.head;                         // 创建指针(对象引用)while(pointer.next.next!=null){pointer = pointer.next;                      // 指向下一个,直至指向倒数第二个结点};let last:Lnode = pointer.nextpointer.next = nullthis.length = this.length - 1;if(this.length==0){this.head=null};return last;}}pop_left():Lnode{/*** 弹出最左边的一个结点并返回* @returns node {Lnode} 原链表最左侧的一个结点*/if(this.head==null){throw "PopEmptyError: Unable to pop up node from an empty linked list.";}else{let node = this.head;this.head = this.head.next;                  // 头结点右移一个node.next = null;                            // 斩断后继联系this.length = this.length - 1;               // 更新链表长度if(this.length==0){this.head=null};return node;}}merge(list:LinkList):void{/** * 实现两个链表的合并* 传入另外一个链表,将这个新传入的链表挂载到本链表的后边* @param list{LinkList} 将要合并到本链表的一个新链表*/let pointer:Lnode = this.head;      // 指向本链头链表指针,实际上是对链表结点对象的引用while(pointer.next!=null){          // 先将指针移动到本链表的尾部pointer = pointer.next;}pointer.next = list.head;           // 将本链表的链尾结点指向新链表的链头结点}reverse():void{/*** 将原链表反转,不反返回何结果* 该方法将会改变原链表*/if(this.head == null){this.head = null;}else{let result:Lnode = new Lnode();while(this.head.next!=null){let node = this.pop_left();         // 新结点为新倒出来的头if(!result.empty){                  // 第一次无先前链头,跳过之node.next = result;             // 新结点后继为之前的头链}result = node;                      // 新头链成为旧头链}// 挂载最后一个头this.head = this.pop_left();this.head.next = result;}}getReverse():LinkList{/*** 返回原链表反转后的新链表* 该方法不会改变原链表*/let result: LinkList = new LinkList();let nodes: Lnode[] = [];nodes = this.toArray().reverse();for(let i=0; i<nodes.length; i++){result.append(nodes[i])}return result}
}

4. 两个链表的合并

要合并两个链表无非是将一个链表的尾元结点的后继指针由null改为指向另外一个链表的首元结点。
在上面的新代码中,我们已经包含了两个链表合并的merge()方法。为了方便起见,将它拿出来单独展示在此:

merge(list:LinkList):void{/** * 实现两个链表的合并* 传入另外一个链表,将这个新传入的链表挂载到本链表的后边* @param list{LinkList} 将要合并到本链表的一个新链表*/let pointer:Lnode = this.head;      // 指向本链头链表指针,实际上是对链表结点对象的引用while(pointer.next!=null){          // 先将指针移动到本链表的尾部pointer = pointer.next;}pointer.next = list.head;           // 将本链表的链尾结点指向新链表的链头结点
}

代码测试:

let list_1 = new LinkList([1,2,3]);      // 构造第1个链表
let list_2 = new LinkList([4,5,6]);      // 构造第2个链表list_1.merge(list_2);                    // 用第1个链表吸收第二个链表的元素
console.log(list_1.toArray());

Out[]:

[Lnode { empty: false, data: 1, next: null },Lnode { empty: false, data: 2, next: null },Lnode { empty: false, data: 3, next: null },Lnode { empty: false, data: 4, next: null },Lnode { empty: false, data: 5, next: null },Lnode { empty: false, data: 6, next: null }
]

可以看到,第二个链表中的结点元素被依序并入了第一个链表中。

TypeScript算法专题 - blog3 - 对TypeScript链表实现中的一些问题总结与改进相关推荐

  1. TypeScript算法专题 - blog1.基于TypeScript语言的单链表实现

    TypeScript算法专题 - 基于TypeScript语言的单链表实现 李俊才 CSDN:jcLee95 邮箱:291148484@163.com 专题目录:https://blog.csdn.n ...

  2. TypeScript算法专题 - [双链表1] - 双链的概念及其实现

    TypeScript算法专题 - [双链表1] 双链的概念及其实现 李俊才 CSDN:jcLee95 邮箱:291148484@163.com 专题目录:https://blog.csdn.net/q ...

  3. TypeScript算法专题 - blog4 - 单链表节点的两-两翻转(两两一组逆序)

    TypeScript数据结构与算法专题 - [单链表4] 单链表节点的`两-两`反转的实现 李俊才 CSDN:jcLee95 邮箱:291148484@163.com 专题目录:https://blo ...

  4. TypeScript算法专题 - blog2 - 单链表节点的索引、结点删除与链表反转

    TypeScript算法专题 - [单链表2] 单链表节点的索引.结点删除与链表反转 李俊才 CSDN:jcLee95 邮箱:291148484@163.com 专题目录:https://blog.c ...

  5. 设计一个算法,删除一个单链表L中元素值最大的结点(假设最大值结点是唯一的)

    设计一个算法,删除一个单链表L中元素值最大的结点(假设最大值结点是唯一的). #include <stdio.h> #include<malloc.h> typedef str ...

  6. TypeScript算法专题 - blog9 - 单链表统计 : 返回指定值在单链表结点中的出现次数

    TypeScript数据结构与算法专题 - [单链表9] 单链表统计 : 返回指定值在单链表结点中的出现次数 李俊才 CSDN:jcLee95 邮箱:291148484@163.com 专题目录:ht ...

  7. TypeScript算法专题 - blog5 - 单链表节点的`任意k个分组反转`的实现

    TypeScript数据结构与算法专题 - [单链表5] 单链表节点的`任意分组反转`实现 李俊才 CSDN:jcLee95 邮箱:291148484@163.com 专题目录:https://blo ...

  8. 数据结构和算法设计专题之---判断单链表中是否有环,环的长度,环的入口节点...

    题目: 给定一个单链表,只给出头指针head: 1.如何判断是否存在环? 2.如何知道环的长度? 3.如何找出环的连接点在哪里? 4.带环链表的长度是多少?   解法: 1.对于问题1,使用追赶的方法 ...

  9. NOIp 图论算法专题总结 (1):最短路、最小生成树、最近公共祖先

    系列索引: NOIp 图论算法专题总结 (1) NOIp 图论算法专题总结 (2) NOIp 图论算法专题总结 (3) 最短路 Floyd 基本思路:枚举所有点与点的中点,如果从中点走最短,更新两点间 ...

最新文章

  1. linux 565显示格式,RGB565转BMP格式 C语言程序
  2. Windows Phone 7 位图编程
  3. Python 字符串按固定长度拆分
  4. 响应式编程RxJava (一)
  5. python apscheduler一次只有一个job_Python使用APScheduler实现定时任务过程解析
  6. 编程高手之路——闭包函数
  7. flutter 泛型_flutter用NestedScrollView的项目必须知道的坑
  8. *HDU1846HDU2188 巴什博奕
  9. mysql 单元测试_优学院MySQL数据库设计单元测试答案
  10. BZOJ2001 HNOI2010 城市建设
  11. 会员积分商城系统的功能介绍
  12. Java开发手册之二方库依赖
  13. [分类] Jzoj P1320 拯救奶牛
  14. 游戏服务器网络出现波动排查方法
  15. Redis---客户端和服务端
  16. xshell进入桌面_xshell中启动linux图形界面
  17. Asterisk manager API(AMI)文档(中文版)
  18. awk oracle,工具: ass109.awk 分析 Oracle 的跟踪文件
  19. C语言文件的读取和写入
  20. 一个食品专业本科生的自白:能不吃最好别吃

热门文章

  1. 【贪心】LeetCode 55. Jump Game
  2. 程序员面试金典——4.1二叉树平衡检查
  3. 二叉树插入算法的非递归版本
  4. 小白入门视频处理笔记:1. *.avi文件读入matlab后的数据结构
  5. [课堂实践与项目]IOS优先级的计算器
  6. Golang map的底层实现
  7. HDU5763 another meaning -(KMP+DP)
  8. AngularJS+ASP.NET MVC+SignalR实现消息推送
  9. STM32用IAR调试出现Error[Pe020]: identifier FILE is undefined 解决方法
  10. Running SharePoint on Windows 7(转)