前面博客我們?cè)谥v解數(shù)組中蛔溃,知道數(shù)組作為數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)有一定的缺陷。在無(wú)序數(shù)組中篱蝇,搜索性能差,在有序數(shù)組中徽曲,插入效率又很低零截,而且這兩種數(shù)組的刪除效率都很低,并且數(shù)組在創(chuàng)建后秃臣,其大小是固定了涧衙,設(shè)置的過(guò)大會(huì)造成內(nèi)存的浪費(fèi),過(guò)小又不能滿足數(shù)據(jù)量的存儲(chǔ)奥此。
本篇博客我們將講解一種新型的數(shù)據(jù)結(jié)構(gòu)——鏈表弧哎。我們知道數(shù)組是一種通用的數(shù)據(jù)結(jié)構(gòu),能用來(lái)實(shí)現(xiàn)棧稚虎、隊(duì)列等很多數(shù)據(jù)結(jié)構(gòu)撤嫩。而鏈表也是一種使用廣泛的通用數(shù)據(jù)結(jié)構(gòu),它也可以用來(lái)作為實(shí)現(xiàn)棧蠢终、隊(duì)列等數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ)序攘,基本上除非需要頻繁的通過(guò)下標(biāo)來(lái)隨機(jī)訪問(wèn)各個(gè)數(shù)據(jù),否則很多使用數(shù)組的地方都可以用鏈表來(lái)代替寻拂。
但是我們需要說(shuō)明的是程奠,鏈表是不能解決數(shù)據(jù)存儲(chǔ)的所有問(wèn)題的,它也有它的優(yōu)點(diǎn)和缺點(diǎn)祭钉。本篇博客我們介紹幾種常見(jiàn)的鏈表瞄沙,分別是單向鏈表、雙端鏈表、有序鏈表距境、雙向鏈表以及有迭代器的鏈表泛粹。并且會(huì)講解一下抽象數(shù)據(jù)類型(ADT)的思想,如何用 ADT 描述棧和隊(duì)列肮疗,如何用鏈表代替數(shù)組來(lái)實(shí)現(xiàn)棧和隊(duì)列晶姊。
1、鏈表(Linked List)
鏈表通常由一連串節(jié)點(diǎn)組成伪货,每個(gè)節(jié)點(diǎn)包含任意的實(shí)例數(shù)據(jù)(data fields)和一或兩個(gè)用來(lái)指向上一個(gè)/或下一個(gè)節(jié)點(diǎn)的位置的鏈接("links")
鏈表(Linked list)是一種常見(jiàn)的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)们衙,是一種線性表,但是并不會(huì)按線性的順序存儲(chǔ)數(shù)據(jù)碱呼,而是在每一個(gè)節(jié)點(diǎn)里存到下一個(gè)節(jié)點(diǎn)的指針(Pointer)蒙挑。
使用鏈表結(jié)構(gòu)可以克服數(shù)組鏈表需要預(yù)先知道數(shù)據(jù)大小的缺點(diǎn),鏈表結(jié)構(gòu)可以充分利用計(jì)算機(jī)內(nèi)存空間愚臀,實(shí)現(xiàn)靈活的內(nèi)存動(dòng)態(tài)管理忆蚀。但是鏈表失去了數(shù)組隨機(jī)讀取的優(yōu)點(diǎn),同時(shí)鏈表由于增加了結(jié)點(diǎn)的指針域姑裂,空間開(kāi)銷比較大馋袜。
2、單向鏈表(Single-Linked List)
單鏈表是鏈表中結(jié)構(gòu)最簡(jiǎn)單的舶斧。一個(gè)單鏈表的節(jié)點(diǎn)(Node)分為兩個(gè)部分欣鳖,第一個(gè)部分(data)保存或者顯示關(guān)于節(jié)點(diǎn)的信息,另一個(gè)部分存儲(chǔ)下一個(gè)節(jié)點(diǎn)的地址茴厉。最后一個(gè)節(jié)點(diǎn)存儲(chǔ)地址的部分指向空值泽台。
單向鏈表只可向一個(gè)方向遍歷,一般查找一個(gè)節(jié)點(diǎn)的時(shí)候需要從第一個(gè)節(jié)點(diǎn)開(kāi)始每次訪問(wèn)下一個(gè)節(jié)點(diǎn)矾缓,一直訪問(wèn)到需要的位置怀酷。而插入一個(gè)節(jié)點(diǎn),對(duì)于單向鏈表嗜闻,我們只提供在鏈表頭插入蜕依,只需要將當(dāng)前插入的節(jié)點(diǎn)設(shè)置為頭節(jié)點(diǎn),next指向原頭節(jié)點(diǎn)即可泞辐。刪除一個(gè)節(jié)點(diǎn)笔横,我們將該節(jié)點(diǎn)的上一個(gè)節(jié)點(diǎn)的next指向該節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)。
在表頭增加節(jié)點(diǎn):
刪除節(jié)點(diǎn):
①咐吼、單向鏈表的具體實(shí)現(xiàn)
package com.ys.datastructure;
public class SingleLinkedList {
private int size;//鏈表節(jié)點(diǎn)的個(gè)數(shù)
private Node head;//頭節(jié)點(diǎn)
public SingleLinkedList(){
size = 0;
head = null;
}
//鏈表的每個(gè)節(jié)點(diǎn)類
private class Node{
private Object data;//每個(gè)節(jié)點(diǎn)的數(shù)據(jù)
private Node next;//每個(gè)節(jié)點(diǎn)指向下一個(gè)節(jié)點(diǎn)的連接
public Node(Object data){
this.data = data;
}
}
//在鏈表頭添加元素
public Object addHead(Object obj){
Node newHead = new Node(obj);
if(size == 0){
head = newHead;
}else{
newHead.next = head;
head = newHead;
}
size++;
return obj;
}
//在鏈表頭刪除元素
public Object deleteHead(){
Object obj = head.data;
head = head.next;
size--;
return obj;
}
//查找指定元素吹缔,找到了返回節(jié)點(diǎn)Node,找不到返回null
public Node find(Object obj){
Node current = head;
int tempSize = size;
while(tempSize > 0){
if(obj.equals(current.data)){
return current;
}else{
current = current.next;
}
tempSize--;
}
return null;
}
//刪除指定的元素锯茄,刪除成功返回true
public boolean delete(Object value){
if(size == 0){
return false;
}
Node current = head;
Node previous = head;
while(current.data != value){
if(current.next == null){
return false;
}else{
previous = current;
current = current.next;
}
}
//如果刪除的節(jié)點(diǎn)是第一個(gè)節(jié)點(diǎn)
if(current == head){
head = current.next;
size--;
}else{//刪除的節(jié)點(diǎn)不是第一個(gè)節(jié)點(diǎn)
previous.next = current.next;
size--;
}
return true;
}
//判斷鏈表是否為空
public boolean isEmpty(){
return (size == 0);
}
//顯示節(jié)點(diǎn)信息
public void display(){
if(size >0){
Node node = head;
int tempSize = size;
if(tempSize == 1){//當(dāng)前鏈表只有一個(gè)節(jié)點(diǎn)
System.out.println("["+node.data+"]");
return;
}
while(tempSize>0){
if(node.equals(head)){
System.out.print("["+node.data+"->");
}else if(node.next == null){
System.out.print(node.data+"]");
}else{
System.out.print(node.data+"->");
}
node = node.next;
tempSize--;
}
System.out.println();
}else{//如果鏈表一個(gè)節(jié)點(diǎn)都沒(méi)有厢塘,直接打印[]
System.out.println("[]");
}
}
}
測(cè)試:
@Test
public void testSingleLinkedList(){
SingleLinkedList singleList = new SingleLinkedList();
singleList.addHead("A");
singleList.addHead("B");
singleList.addHead("C");
singleList.addHead("D");
//打印當(dāng)前鏈表信息
singleList.display();
//刪除C
singleList.delete("C");
singleList.display();
//查找B
System.out.println(singleList.find("B"));
}
打印結(jié)果:
②茶没、用單向鏈表實(shí)現(xiàn)棧
棧的pop()方法和push()方法,對(duì)應(yīng)于鏈表的在頭部刪除元素deleteHead()以及在頭部增加元素addHead()晚碾。
package com.ys.datastructure;
public class StackSingleLink {
private SingleLinkedList link;
public StackSingleLink(){
link = new SingleLinkedList();
}
//添加元素
public void push(Object obj){
link.addHead(obj);
}
//移除棧頂元素
public Object pop(){
Object obj = link.deleteHead();
return obj;
}
//判斷是否為空
public boolean isEmpty(){
return link.isEmpty();
}
//打印棧內(nèi)元素信息
public void display(){
link.display();
}
}
4抓半、雙端鏈表
對(duì)于單項(xiàng)鏈表,我們?nèi)绻朐谖膊刻砑右粋€(gè)節(jié)點(diǎn)格嘁,那么必須從頭部一直遍歷到尾部笛求,找到尾節(jié)點(diǎn),然后在尾節(jié)點(diǎn)后面插入一個(gè)節(jié)點(diǎn)糕簿。這樣操作很麻煩探入,如果我們?cè)谠O(shè)計(jì)鏈表的時(shí)候多個(gè)對(duì)尾節(jié)點(diǎn)的引用,那么會(huì)簡(jiǎn)單很多懂诗。
注意和后面將的雙向鏈表的區(qū)別7渌浴!殃恒!
①植旧、雙端鏈表的具體實(shí)現(xiàn)
package com.ys.link;
public class DoublePointLinkedList {
private Node head;//頭節(jié)點(diǎn)
private Node tail;//尾節(jié)點(diǎn)
private int size;//節(jié)點(diǎn)的個(gè)數(shù)
private class Node{
private Object data;
private Node next;
public Node(Object data){
this.data = data;
}
}
public DoublePointLinkedList(){
size = 0;
head = null;
tail = null;
}
//鏈表頭新增節(jié)點(diǎn)
public void addHead(Object data){
Node node = new Node(data);
if(size == 0){//如果鏈表為空,那么頭節(jié)點(diǎn)和尾節(jié)點(diǎn)都是該新增節(jié)點(diǎn)
head = node;
tail = node;
size++;
}else{
node.next = head;
head = node;
size++;
}
}
//鏈表尾新增節(jié)點(diǎn)
public void addTail(Object data){
Node node = new Node(data);
if(size == 0){//如果鏈表為空离唐,那么頭節(jié)點(diǎn)和尾節(jié)點(diǎn)都是該新增節(jié)點(diǎn)
head = node;
tail = node;
size++;
}else{
tail.next = node;
tail = node;
size++;
}
}
//刪除頭部節(jié)點(diǎn)病附,成功返回true,失敗返回false
public boolean deleteHead(){
if(size == 0){//當(dāng)前鏈表節(jié)點(diǎn)數(shù)為0
return false;
}
if(head.next == null){//當(dāng)前鏈表節(jié)點(diǎn)數(shù)為1
head = null;
tail = null;
}else{
head = head.next;
}
size--;
return true;
}
//判斷是否為空
public boolean isEmpty(){
return (size ==0);
}
//獲得鏈表的節(jié)點(diǎn)個(gè)數(shù)
public int getSize(){
return size;
}
//顯示節(jié)點(diǎn)信息
public void display(){
if(size >0){
Node node = head;
int tempSize = size;
if(tempSize == 1){//當(dāng)前鏈表只有一個(gè)節(jié)點(diǎn)
System.out.println("["+node.data+"]");
return;
}
while(tempSize>0){
if(node.equals(head)){
System.out.print("["+node.data+"->");
}else if(node.next == null){
System.out.print(node.data+"]");
}else{
System.out.print(node.data+"->");
}
node = node.next;
tempSize--;
}
System.out.println();
}else{//如果鏈表一個(gè)節(jié)點(diǎn)都沒(méi)有侯繁,直接打印[]
System.out.println("[]");
}
}
}
②胖喳、用雙端鏈表實(shí)現(xiàn)隊(duì)列
package com.ys.link;
public class QueueLinkedList {
private DoublePointLinkedList dp;
public QueueLinkedList(){
dp = new DoublePointLinkedList();
}
public void insert(Object data){
dp.addTail(data);
}
public void delete(){
dp.deleteHead();
}
public boolean isEmpty(){
return dp.isEmpty();
}
public int getSize(){
return dp.getSize();
}
public void display(){
dp.display();
}
}
5、抽象數(shù)據(jù)類型(ADT)
在介紹抽象數(shù)據(jù)類型的時(shí)候贮竟,我們先看看什么是數(shù)據(jù)類型,聽(tīng)到這個(gè)詞较剃,在Java中我們可能首先會(huì)想到像 int,double這樣的詞咕别,這是Java中的基本數(shù)據(jù)類型,一個(gè)數(shù)據(jù)類型會(huì)涉及到兩件事:
①写穴、擁有特定特征的數(shù)據(jù)項(xiàng)
②惰拱、在數(shù)據(jù)上允許的操作
比如Java中的int數(shù)據(jù)類型,它表示整數(shù)啊送,取值范圍為:-2147483648~2147483647偿短,還能使用各種操作符,+馋没、-昔逗、*、/ 等對(duì)其操作篷朵。數(shù)據(jù)類型允許的操作是它本身不可分離的部分勾怒,理解類型包括理解什么樣的操作可以應(yīng)用在該類型上婆排。
那么當(dāng)年設(shè)計(jì)計(jì)算機(jī)語(yǔ)言的人,為什么會(huì)考慮到數(shù)據(jù)類型笔链?
我們先看這樣一個(gè)例子段只,比如,大家都需要住房子鉴扫,也都希望房子越大越好赞枕。但顯然,沒(méi)有錢(qián)坪创,考慮房子沒(méi)有意義炕婶。于是就出現(xiàn)了各種各樣的商品房,有別墅的误堡、復(fù)式的古话、錯(cuò)層的、單間的……甚至只有兩平米的膠囊房間锁施。這樣做的意義是滿足不同人的需要陪踩。
同樣,在計(jì)算機(jī)中悉抵,也存在相同的問(wèn)題肩狂。計(jì)算1+1這樣的表達(dá)式不需要開(kāi)辟很大的存儲(chǔ)空間,不需要適合小數(shù)甚至字符運(yùn)算的內(nèi)存空間姥饰。于是計(jì)算機(jī)的研究者們就考慮傻谁,要對(duì)數(shù)據(jù)進(jìn)行分類,分出來(lái)多種數(shù)據(jù)類型列粪。比如int审磁,比如float。
雖然不同的計(jì)算機(jī)有不同的硬件系統(tǒng)岂座,但實(shí)際上高級(jí)語(yǔ)言編寫(xiě)者才不管程序運(yùn)行在什么計(jì)算機(jī)上态蒂,他們的目的就是為了實(shí)現(xiàn)整形數(shù)字的運(yùn)算,比如a+b等费什。他們才不關(guān)心整數(shù)在計(jì)算機(jī)內(nèi)部是如何表示的钾恢,也不管CPU是如何計(jì)算的。于是我們就考慮鸳址,無(wú)論什么計(jì)算機(jī)瘩蚪、什么語(yǔ)言都會(huì)面臨類似的整數(shù)運(yùn)算,我們可以考慮將其抽象出來(lái)稿黍。抽象是抽取出事物具有的普遍性本質(zhì)疹瘦,是對(duì)事物的一個(gè)概括,是一種思考問(wèn)題的方式闻察。
抽象數(shù)據(jù)類型(ADT)是指一個(gè)數(shù)學(xué)模型及定義在該模型上的一組操作拱礁。它僅取決于其邏輯特征琢锋,而與計(jì)算機(jī)內(nèi)部如何表示和實(shí)現(xiàn)無(wú)關(guān)。比如剛才說(shuō)得整型呢灶,各個(gè)計(jì)算機(jī)吴超,不管大型機(jī)、小型機(jī)鸯乃、PC鲸阻、平板電腦甚至智能手機(jī),都有“整型”類型缨睡,也需要整形運(yùn)算鸟悴,那么整型其實(shí)就是一個(gè)抽象數(shù)據(jù)類型。
更廣泛一點(diǎn)的奖年,比如我們剛講解的棧和隊(duì)列這兩種數(shù)據(jù)結(jié)構(gòu)细诸,我們分別使用了數(shù)組和鏈表來(lái)實(shí)現(xiàn),比如棧陋守,對(duì)于使用者只需要知道pop()和push()方法或其它方法的存在以及如何使用即可震贵,使用者不需要知道我們是使用的數(shù)組或是鏈表來(lái)實(shí)現(xiàn)的。
ADT的思想可以作為我們?cè)O(shè)計(jì)工具的理念水评,比如我們需要存儲(chǔ)數(shù)據(jù)猩系,那么就從考慮需要在數(shù)據(jù)上實(shí)現(xiàn)的操作開(kāi)始,需要存取最后一個(gè)數(shù)據(jù)項(xiàng)嗎中燥?還是第一個(gè)寇甸?還是特定值的項(xiàng)?還是特定位置的項(xiàng)疗涉?回答這些問(wèn)題會(huì)引出ADT的定義拿霉,只有完整的定義了ADT后,才應(yīng)該考慮實(shí)現(xiàn)的細(xì)節(jié)咱扣。
這在我們Java語(yǔ)言中的接口設(shè)計(jì)理念是想通的友浸。
6、有序鏈表
前面的鏈表實(shí)現(xiàn)插入數(shù)據(jù)都是無(wú)序的偏窝,在有些應(yīng)用中需要鏈表中的數(shù)據(jù)有序,這稱為有序鏈表武学。
在有序鏈表中祭往,數(shù)據(jù)是按照關(guān)鍵值有序排列的。一般在大多數(shù)需要使用有序數(shù)組的場(chǎng)合也可以使用有序鏈表火窒。有序鏈表優(yōu)于有序數(shù)組的地方是插入的速度(因?yàn)樵夭恍枰苿?dòng))硼补,另外鏈表可以擴(kuò)展到全部有效的使用內(nèi)存,而數(shù)組只能局限于一個(gè)固定的大小中熏矿。
package com.ys.datastructure;
public class OrderLinkedList {
private Node head;
private class Node{
private int data;
private Node next;
public Node(int data){
this.data = data;
}
}
public OrderLinkedList(){
head = null;
}
//插入節(jié)點(diǎn)已骇,并按照從小打到的順序排列
public void insert(int value){
Node node = new Node(value);
Node pre = null;
Node current = head;
while(current != null && value > current.data){
pre = current;
current = current.next;
}
if(pre == null){
head = node;
head.next = current;
}else{
pre.next = node;
node.next = current;
}
}
//刪除頭節(jié)點(diǎn)
public void deleteHead(){
head = head.next;
}
public void display(){
Node current = head;
while(current != null){
System.out.print(current.data+" ");
current = current.next;
}
System.out.println("");
}
}
在有序鏈表中插入和刪除某一項(xiàng)最多需要O(N)次比較离钝,平均需要O(N/2)次,因?yàn)楸仨氀刂湵砩弦徊揭徊阶卟拍苷业秸_的插入位置褪储,然而可以最快速度刪除最值卵渴,因?yàn)橹恍枰獎(jiǎng)h除表頭即可,如果一個(gè)應(yīng)用需要頻繁的存取最小值鲤竹,且不需要快速的插入浪读,那么有序鏈表是一個(gè)比較好的選擇方案。比如優(yōu)先級(jí)隊(duì)列可以使用有序鏈表來(lái)實(shí)現(xiàn)辛藻。
7碘橘、有序鏈表和無(wú)序數(shù)組組合排序
比如有一個(gè)無(wú)序數(shù)組需要排序,前面我們?cè)谥v解冒泡排序吱肌、選擇排序痘拆、插入排序這三種簡(jiǎn)單的排序時(shí),需要的時(shí)間級(jí)別都是O(N2)氮墨。
現(xiàn)在我們講解了有序鏈表之后纺蛆,對(duì)于一個(gè)無(wú)序數(shù)組,我們先將數(shù)組元素取出勇边,一個(gè)一個(gè)的插入到有序鏈表中犹撒,然后將他們從有序鏈表中一個(gè)一個(gè)刪除,重新放入數(shù)組粒褒,那么數(shù)組就會(huì)排好序了识颊。和插入排序一樣,如果插入了N個(gè)新數(shù)據(jù)奕坟,那么進(jìn)行大概N2/4次比較祥款。但是相對(duì)于插入排序,每個(gè)元素只進(jìn)行了兩次排序月杉,一次從數(shù)組到鏈表刃跛,一次從鏈表到數(shù)組,大概需要2*N次移動(dòng)苛萎,而插入排序則需要N2次移動(dòng)桨昙,
效率肯定是比前面講的簡(jiǎn)單排序要高,但是缺點(diǎn)就是需要開(kāi)辟差不多兩倍的空間腌歉,而且數(shù)組和鏈表必須在內(nèi)存中同時(shí)存在蛙酪,如果有現(xiàn)成的鏈表可以用,那么這種方法還是挺好的翘盖。
8桂塞、雙向鏈表
我們知道單向鏈表只能從一個(gè)方向遍歷,那么雙向鏈表它可以從兩個(gè)方向遍歷馍驯。
具體代碼實(shí)現(xiàn):
package com.ys.datastructure;
public class TwoWayLinkedList {
private Node head;//表示鏈表頭
private Node tail;//表示鏈表尾
private int size;//表示鏈表的節(jié)點(diǎn)個(gè)數(shù)
private class Node{
private Object data;
private Node next;
private Node prev;
public Node(Object data){
this.data = data;
}
}
public TwoWayLinkedList(){
size = 0;
head = null;
tail = null;
}
//在鏈表頭增加節(jié)點(diǎn)
public void addHead(Object value){
Node newNode = new Node(value);
if(size == 0){
head = newNode;
tail = newNode;
size++;
}else{
head.prev = newNode;
newNode.next = head;
head = newNode;
size++;
}
}
//在鏈表尾增加節(jié)點(diǎn)
public void addTail(Object value){
Node newNode = new Node(value);
if(size == 0){
head = newNode;
tail = newNode;
size++;
}else{
newNode.prev = tail;
tail.next = newNode;
tail = newNode;
size++;
}
}
//刪除鏈表頭
public Node deleteHead(){
Node temp = head;
if(size != 0){
head = head.next;
head.prev = null;
size--;
}
return temp;
}
//刪除鏈表尾
public Node deleteTail(){
Node temp = tail;
if(size != 0){
tail = tail.prev;
tail.next = null;
size--;
}
return temp;
}
//獲得鏈表的節(jié)點(diǎn)個(gè)數(shù)
public int getSize(){
return size;
}
//判斷鏈表是否為空
public boolean isEmpty(){
return (size == 0);
}
//顯示節(jié)點(diǎn)信息
public void display(){
if(size >0){
Node node = head;
int tempSize = size;
if(tempSize == 1){//當(dāng)前鏈表只有一個(gè)節(jié)點(diǎn)
System.out.println("["+node.data+"]");
return;
}
while(tempSize>0){
if(node.equals(head)){
System.out.print("["+node.data+"->");
}else if(node.next == null){
System.out.print(node.data+"]");
}else{
System.out.print(node.data+"->");
}
node = node.next;
tempSize--;
}
System.out.println();
}else{//如果鏈表一個(gè)節(jié)點(diǎn)都沒(méi)有阁危,直接打印[]
System.out.println("[]");
}
}
}
我們也可以用雙向鏈表來(lái)實(shí)現(xiàn)雙端隊(duì)列玛痊,這里就不做具體代碼演示了。
9狂打、總結(jié)
上面我們講了各種鏈表擂煞,每個(gè)鏈表都包括一個(gè)LinikedList對(duì)象和許多Node對(duì)象,LinkedList對(duì)象通常包含頭和尾節(jié)點(diǎn)的引用菱父,分別指向鏈表的第一個(gè)節(jié)點(diǎn)和最后一個(gè)節(jié)點(diǎn)颈娜。而每個(gè)節(jié)點(diǎn)對(duì)象通常包含數(shù)據(jù)部分data,以及對(duì)上一個(gè)節(jié)點(diǎn)的引用prev和下一個(gè)節(jié)點(diǎn)的引用next浙宜,只有下一個(gè)節(jié)點(diǎn)的引用稱為單向鏈表官辽,兩個(gè)都有的稱為雙向鏈表。next值為null則說(shuō)明是鏈表的結(jié)尾粟瞬,如果想找到某個(gè)節(jié)點(diǎn)同仆,我們必須從第一個(gè)節(jié)點(diǎn)開(kāi)始遍歷,不斷通過(guò)next找到下一個(gè)節(jié)點(diǎn)裙品,直到找到所需要的俗批。棧和隊(duì)列都是ADT,可以用數(shù)組來(lái)實(shí)現(xiàn)市怎,也可以用鏈表實(shí)現(xiàn)岁忘。