数据结构与算法(第三章)

一、 什么是链表?

链表是一种存储数据集合的数据结构。有以下特性:

1, 相邻元素之间通过指针连接。

2, 最后一个元素的后继指针为NULL。

3, 程序执行中,链表长度可变。

4, 链表的空间可以按需分配

5, 没有内存空间的浪费,只是指针需要额外的内存开销。

二、 什么是数组?

也是存储数据的一种数据结构。

特点:

1, 在常数时间内访问数组之内的元素。这个过程呢就是,为了访问一个数组元素,需要计算其距离数组基地址的偏移量,用一个乘法计算偏移量,加上基地址的值,就是该元素的内存地址。

2, 数组占据的是一个连续的内存块。

3, 数组大小是固定的。

4, 数组插入一个元素操作负责,需要移动该位置后面的所有元素。

三、链表和数组比较

数组随机访问时间为常数级,链表为O(n)级。

链表插入和删除元素比数组更加快捷方便。

链表比数组占用更多的内存空间。

链表大小动态可变,数组固定大小。

四、链表具体介绍

单向链表

我们通常所说的都是单向链表,包含多个结点,每个结点都有指向下一个结点的next指针,链表最后一个结点next指针值为空,表示链表结束。

链表的基本操作:

遍历、链表中插入一个元素、链表中删除一个元素

单向链表插入分三种情况:

链表的表头插入一个结点、链表的中间插入一个结点、链表的末尾插入一个结点。

同样的删除也对应三种情况:

链表的表头删除一个结点、链表的中间删除一个结点、链表的末尾删除一个结点

下面是单向链表的具体示例:

首先是结点的数据结构,包含int类型的数据和ListNode类型的next指针,构造函数以及get/set方法。

public class ListNode {
    private int data;
    private ListNode next;

    public ListNode(int data) {
        this.data = data;
    }

    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    public ListNode getNext() {
        return next;
    }

    public void setNext(ListNode next) {
        this.next = next;
    }
}

接下来是针对单向链表的一些具体操作,包括循环打印结点的值,计算链表的长度、插入以及删除某个结点。

public class ListTest {

    public static void main(String[] args) {
        ListNode listNode = new ListNode(1);
        ListNode listNode1 = new ListNode(2);
        ListNode listNode2 = new ListNode(3);
        ListNode listNode3 = new ListNode(4);
        listNode.setNext(listNode1);
        listNode1.setNext(listNode2);
        listNode2.setNext(listNode3);
        System.out.println(getLength(listNode));
        insertNode(listNode,2,new ListNode(5));
        printList(listNode);
        deleteNode(listNode,2);
        printList(listNode);
    }
    //print list node
    public static void printList(ListNode headNode){
        ListNode listNode = headNode;
        while(listNode != null){
            System.out.println(listNode.getData());
            listNode = listNode.getNext();
        }
    }

    //calculate list length
    public static int getLength(ListNode headNode) {
        int i = 0;
        ListNode currentNode = headNode;
        while (currentNode != null) {
            currentNode = currentNode.getNext();
            i++;
        }
        return i;
    }

    //insert list node
    public static ListNode insertNode(ListNode headNode,int position,ListNode listNode){
        if(headNode ==null){
            return listNode;
        }
        if( position < 1 || position > getLength(headNode) + 1) {
            System.out.println("position invalid");
        }else if(position == 1){
            listNode.setNext(headNode.getNext());
            headNode = listNode;
        }else{
            //include insert in the middle or in the last of the list
            int i = 1;
            ListNode tempNode = headNode;
            while(i < position - 1){
                tempNode = tempNode.getNext();
                i++;
            }
            listNode.setNext(tempNode.getNext());
            tempNode.setNext(listNode);
        }
        return headNode;
    }

    //delete list node
    public static ListNode deleteNode(ListNode headNode,int position){
        if(headNode == null){
            System.out.println("list is empty");
        }
        if( position < 1 || position > getLength(headNode) + 1) {
            System.out.println("position invalid");
            return headNode;
        }else if(position == 1){
            headNode = headNode.getNext();
        }else{
            int i = 1;
            ListNode tempNode = headNode;
            while(i < position -1){
                tempNode = tempNode.getNext();
                i++;
            }
            tempNode.setNext(tempNode.getNext().getNext());
        }
        return headNode;
    }
}

双向链表

在一个双向链表中,一个结点既知道自己的后继结点,也知道自己的前驱结点。

双向链表的缺点:

比单向链表更加占用内存空间,因为需要多一个指针指向前驱结点。

结点的插入、删除更加费时。

双向链表插入分三种情况:

链表的表头插入一个结点、链表的中间插入一个结点、链表的末尾插入一个结点

同样的删除也对应三种情况:

链表的表头删除一个结点、链表的中间删除一个结点、链表的末尾删除一个结点

下面是双向链表的具体示例:

首先是结点的数据结构,包含int类型的数据和ListNode类型的next指针,ListNode类型的pre指针,构造函数以及get/set方法。

public class DoubleListNode {
    private int data;
    private DoubleListNode next;
    private DoubleListNode pre;

    public DoubleListNode(int data) {
        this.data = data;
    }

    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    public DoubleListNode getNext() {
        return next;
    }

    public void setNext(DoubleListNode next) {
        this.next = next;
    }

    public DoubleListNode getPre() {
        return pre;
    }

    public void setPre(DoubleListNode pre) {
        this.pre = pre;
    }
}

接下来是针对双向链表的一些具体操作,包括循环打印结点的值,计算链表的长度、插入以及删除某个结点。

public class DoubleListTest {

    public static void main(String[] args) {

        DoubleListNode listNode = new DoubleListNode(1);
        DoubleListNode listNode1 = new DoubleListNode(2);
        DoubleListNode listNode2 = new DoubleListNode(3);
        DoubleListNode listNode3 = new DoubleListNode(4);
        listNode.setNext(listNode1);
        listNode1.setPre(listNode);
        listNode1.setNext(listNode2);
        listNode2.setPre(listNode1);
        listNode2.setNext(listNode3);
        listNode3.setPre(listNode2);
        System.out.println(getLength(listNode));
        insertNode(listNode,3,new DoubleListNode(9));
        printDoubleList(listNode);
        deleteNode(listNode,3);
        printDoubleList(listNode);
    }
    
    //print list node data
    public static void printDoubleList(DoubleListNode headNode){
        DoubleListNode listNode = headNode;
        while(listNode != null){
            System.out.println(listNode.getData());
            listNode = listNode.getNext();
        }
    }

    //calculate list length
    public static int getLength(DoubleListNode headNode) {
        int i = 0;
        DoubleListNode currentNode = headNode;
        while (currentNode != null) {
            currentNode = currentNode.getNext();
            i++;
        }
        return i;
    }

    //insert list node
    public static DoubleListNode insertNode(DoubleListNode headNode, int position, DoubleListNode listNode) {
        if (headNode == null) {
            return listNode;
        }
        if (position < 1 || position > getLength(headNode) + 1) {
            System.out.println("position invalid");
        } else if (position == 1) {
            listNode.setNext(headNode);
            headNode.setPre(listNode);
            return listNode;
        } else {
            int i = 1;
            DoubleListNode tempNode = headNode;
            while (i < position - 1) {
                tempNode = tempNode.getNext();
                i++;
            }
            listNode.setNext(tempNode.getNext());
            // if insert in the last of the list, avoid to case nullpointer
            if (tempNode.getNext() != null) {
                tempNode.getNext().setPre(listNode);
            }
            listNode.setPre(tempNode);
            tempNode.setNext(listNode);
        }
        return headNode;
    }

    //delete list node
    public static DoubleListNode deleteNode(DoubleListNode headNode, int position) {
        if (headNode == null) {
            return headNode;
        }
        if (position < 1 || position > getLength(headNode) + 1) {
            System.out.println("invalid position");
        } else if (position == 1) {
            DoubleListNode listNode = headNode.getNext();
            headNode = null;
            return listNode;
        } else {
            int i = 1;
            DoubleListNode tempNode = headNode;
            while (i < position - 1) {
                tempNode = tempNode.getNext();
                i++;
            }
            DoubleListNode pNode = tempNode.getNext();
            // if delete in the last of the list, avoid to case nullpointer
            if (pNode.getNext() != null) {
                tempNode.setNext(pNode.getNext());
            }
            pNode.getNext().setPre(tempNode);
            pNode = null;
        }
        return headNode;
    }
}

循环链表

和单向链表、双向链表都以null结尾不同,循环链表没有结束标志。轮询算法可以用循环链表来实现。

下面举两个循环链表的基本操作,遍历和计算长度。循环链表的结点和单向链表结点数据结构一致。这里不再给出。

public class CircleLIstTest {

    public static void main(String[] args) {
        ListNode listNode = new ListNode(1);
        ListNode listNode1 = new ListNode(2);
        ListNode listNode2 = new ListNode(3);
        ListNode listNode3 = new ListNode(4);
        listNode.setNext(listNode1);
        listNode1.setNext(listNode2);
        listNode2.setNext(listNode3);
        listNode3.setNext(listNode);
        System.out.println(getCircleListLength(listNode));
        printCircleListLength(listNode);
    }
    
    // calculate list length    
    public static int getCircleListLength(ListNode headNode){
        ListNode listNode = headNode;
        int length = 0;
        while(listNode != null){
            listNode = listNode.getNext();
            length++;
            if(listNode == headNode){
                break;
            }
        }
        return length;
    }
    //print list node data
    public static void printCircleListLength(ListNode headNode){
        ListNode listNode = headNode;
        while(listNode != null){
            System.out.println(listNode.getData());
            listNode = listNode.getNext();
            if(listNode == headNode){
                break;
            }
        }
    }
}

前面已经介绍了单向链表、双向链表、循环链表以及他们的一些基本操作。下面是给出针对链表的一些问题,其中一些巧妙地思路值得学习。

问题1:找到单向链表中的倒数第n个结点。

这个问题我们可能首先想到的就是先遍历整个链表得到链表的长度(假设为m),然后在重新遍历找到第m-n+1个就是倒数第n个结点。这里的时间复杂度是O(2m-n+1)。

那么是否有更加快捷的方法呢?答案是有的。

我们可以使用散列表,也就是哈希表。哈希表可以想象成一个java中的map。我们用结点的顺序值作为key,结点作为value。那么只需要遍历一次链表,生成哈希表。然后立马就可以根据key值m-n+1,得到这个结点。这个方法时间复杂度是O(m+1)。空间复杂度为O(m)。

除了第二个方法,是否有只扫描一次链表就解决问题的方法?

这里我们采用两个指针,开始都指向链表头结点。然后让其中一个指针先走n步,然后两个指针一起往下走,当一个指针到达末尾,另外一个正好处于倒数第n个节点处。时间复杂度为O(2m-n+1)。下面是使用两个指针时的代码。

ListNode getNNode(ListNode headNode, int n) {
        ListNode listNode = headNode;
        ListNode tempNode = null;
        for (int i = 1; i < n; i++) {
            if (listNode != null) {
                listNode = listNode.getNext();
            }
        }
        while (listNode != null) {
            if (tempNode == null) {
                tempNode = headNode;
            }else{
                tempNode = tempNode.getNext();
            }
            listNode = listNode.getNext();
        }
        if(tempNode != null){
            return tempNode;
        }
        return null;
    }

问题二:假设两个单链表在某个结点相交后,成为一个单向链表,两个链表的表头结点是已知的,相交的结点未知,相交之前各自的结点数是未知的,可能相同也可能不同。设计算法找到两个链表的合并点。

这里给出一种方法。

先分别遍历两个链表获得其长度(假设m,n且m>n),然后求出两个的差值(m-n),从较长的一个链表先走(m-n)步,然后两个链表同时遍历,直到出现两个后继指针相等的情况。则该结点就是相交结点。

public ListNode getIntersectNode(ListNode list1,ListNode list2){
        int i=0,j=0,sub;
        ListNode temp1 = list1;
        ListNode temp2 = list2;
        while(temp1 != null){
            i++;
            temp1 = temp1.getNext();
        }
        while(temp2 != null){
            j++;
            temp2 = temp2.getNext();
        }
        if(i < j){
            temp1 = list2;
            temp2 = list1;
            sub = j - i;
        }else{
            temp1 = list1;
            temp2 = list2;
            sub = i - j;
        }
        for(int k=0;k<sub;k++){
            temp1 = temp1.getNext();
        }
        while(temp1 != null && temp2 != null){
            if(temp1 == temp2){
                return temp1;
            }
            temp1 = temp1.getNext();
            temp2 = temp2.getNext();
        }
        return null;
    }

问题三:约瑟夫环,N个人想选出一个领头人,他们排成一个环,沿着环每数到第M个人,就从环中排除该人,并从下一个人重新计数,找出最后留在环中的人。

这里我们就可以用循环链表来解决这个问题,假设输入时一个有N个结点的循环链表,每个结点依次有一个编号(1-N)。代码如下:

public static ListNode getPosition(int N, int M){
        ListNode p = new ListNode(1);
        ListNode q;
        q = p;
        for(int i=2;i<=N;i++){
            ListNode r = new ListNode(i);
            p.setNext(r);
            p = r;
        }
        p.setNext(q);
        for(int j=N;j>1;j--){
            for(int k=0;k<M - 1;k++){
                p = p.getNext();
            }
            p.setNext(p.getNext().getNext());
        }
        System.out.println("the result is:" + p.getData());
        return p;
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容

  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,818评论 0 13
  • 目录 1、属性 2、链表和数组的区别 2.1、数组概述 2.2、数组和链表优缺点 2.3、链表和数组的比较 3、单...
    我哈啊哈啊哈阅读 2,806评论 1 41
  • 原文地址:http://theory.stanford.edu/~amitp/GameProgramming/ 1...
    达微阅读 19,461评论 0 28
  • 有人开玩笑说,想改变自己的想法可以去三个地方,火葬场、医院、奢侈品店。其他两个地点我无法印证,至少医院在我看来就是...
    阳光灿烂的心情阅读 370评论 0 0
  • 2017年,发现自己因为胆怯错失了很多东西。 高考头一天,和妈妈大吵,第二天中午也没有缓和,老妈在送我进考场时,我...
    As冯丽华阅读 239评论 2 2