Java面试笔试宝典 - 第 8 章 数据结构与算法:8.1 链表

玩转数据结构:第5章 链表和递归

Java面试笔试宝典 - 第 8 章 数据结构与算法

8.1 链表

8.1.1 如何实现单链表的增删操作

链表作为最基本的(线性的、动态的)数据结构,在程序设计中有着非常重要的作用,其存储特点如下:可以用任意一组存储单元来存储单链表中的数据元素(存储单元可以是不连续的),而且,除了存储每个数据元素 ai的值以外,还必须存储指示其直接后继元素的信息。这两部分信息组成的数据元素 ai的存储映像称为结点。N 个结点链在一块被称为链表,当结点只包含其后继结点的信息的链表就被称为单链表,在内存中存储的方式如图 8-1 所示。

在 Java 语言中,可以定义如下的数据类来存储结点信息。

public class Node<E> {
    Node next = null;
    E data;

    public Node(E data) {
        this.data = data;
    }
}

链表最重要的操作就是向链表中插入元素和从链表中删除元素。

单链表的插入操作是将值为 x 的新结点插入到单链表的第 i 个结点的位置上,即插入到数据元素 ai-1 与 ai 之间。其具体步骤如下:

1)找到 ai-1 的引用(存储位置)p。

2)生成一个数据域为 x 的新结点 s。

3)设置 p.next=s。

4)设置 s.next=ai。

图 8-2 为单链表插入结点示意图。

单链表的删除操作是将单链表的第 i 个结点删去。其具体步骤如下:

1)找到 ai-1的存储位置 p。

2)令 p.next 指向 ai的直接后继结点(即把 ai从链上摘下)ai+1。

图 8-3 为单链表删除结点示意图。

下面链表操作的示例给出了链表的基本操作。

package cn.bjut.content8.test1;

public class MyLinkedList {
  Node head = null; //链表头的引用

    /**
     * 向链表中插入数据
     * @param d:插入数据的内容
     */
  public void addNode(int d){
      Node newNode = new Node(d);
      if(head == null){
          head = newNode;
          return;
      }
      Node tmp = head;
      while (tmp.next!=null){
          tmp = tmp.next;//
      }
      //add node to end
      tmp.next = newNode;
  }
  public Boolean deleteNode(int index){
      if(index<1 || index>length()){  //删除的位置不合理
          return false;
      }
      //删除链表第一个元素
      if(index == 1){
          head = head.next;
          return true;
      }
      int i = 2;
      Node preNode = head;
      Node curNode = preNode.next;
      while (curNode!=null){
          if(i==index){
              preNode.next = curNode.next;
              return true;
          }
          preNode = curNode;
          curNode = curNode.next;
          i++;
      }
      return true;
  }

    /**
     * 返回结点的长度
     * @return
     */
  public int length(){
      int length=0;
      Node tmp = head;
      while (tmp!=null){
          length ++;
          tmp=tmp.next;
      }
      return length;
  }

    /**
     * 对链表进行排序
     * 返回排序后的头结点
     */
  public Node orderList(){
      Node nextNode = null;
      int temp =0;
      Node curNode=head;
      while(curNode.next!=null){
          nextNode =curNode.next;
          while (nextNode!=null){
              if((int)curNode.data > (int)nextNode.data){
                  temp = (int)curNode.data;
                  curNode.data=nextNode.data;
                  nextNode.data=temp;
              }
              nextNode=nextNode.next;
          }
          curNode=curNode.next;
      }
      return head;
  }
  public void printList(){
      Node tmp = head;
      while (tmp!=null){
          System.out.println(tmp.data);
          tmp=tmp.next;
      }
  }

    public static void main(String[] args) {
        MyLinkedList list = new MyLinkedList();
        list.addNode(5);
        list.addNode(3);
        list.addNode(1);
        list.addNode(3);
        System.out.println("listLen"+list.length());
        System.out.println("before order");
        list.printList();
        list.orderList();
        System.out.println("after order");
        list.printList();

    }

}

上述程序运行结果为:

以上这个例子主要实现了链表的最基本的操作,这些操作包括给链表增加结点(每次都把新增加的结点加到链表尾部)和删除链表中的结点和计算链表的长度。此外还通过插入排序算法实现了对链表的排序。

8.1.2 如何从链表中删除重复数据

如何从链表中删除重复数据,最容易想到的方法就是遍历链表,把遍历到的值存储到一个 Hashtable 中,在遍历过程中,若当前访问的值在 Hashtable 中已经存在,则说明这个数据是重复的,因此就可以删除。具体实现代码如下:

    /**
     * 删除链表中重复结点
     * @param head
     */
    public void deleteDuplecate(Node head){
        Hashtable<Object,Integer> table=new Hashtable<>();
        Node tmp = head;
        Node pre = null;
        while (tmp!=null){
            if(table.containsKey(tmp.data))
                pre.next=tmp.next;
            else {
                table.put(tmp.data,1);
                pre = tmp;
            }
            tmp =tmp.next;
        }
    }

以上这种方法的优点是时间复杂度较低,但也有一个很明显的缺点,就是在遍历过程中需要额外的存储空间来保存已遍历过的值。是否还有更加高效的算法呢?下面介绍另外一种不需要额外存储空间的算法。

这种方法的主要思路为对链表进行双重循环遍历,外循环正常遍历链表,假设外循环当前遍历的结点为 cur,内循环从 cur 开始遍历,若碰到与 cur 所指向结点值相同,则删除这个重复结点。算法的实现方法如下:

以上方法的优点是不需要额外的存储空间,缺点也很明显,时间复杂度比上面介绍的算法的时间复杂度高。假设外循环当前遍历的结点为 cur,内循环在遍历的过程中会删除掉与 cur 结点值相同的所有结点。在实现时还可以采用另外一种实现方法:外循环当前遍历的结点为 cur,内循环从链表头开始遍历至 cur,只要碰到与 cur 值相同的结点就删除该结点,同时内循环结束,因为与 cur 相同的结点只可能存在一个(如果存在多个,在前面的遍历过程中已经被删除了)。采用这种方法在特定的数据发布的情况下会提高算法的时间复杂度。 

8.1.3 如何找出单链表中的倒数第 k 个元素

为了找出单链表中的倒数第 k 个元素,最容易想到的方法是首先遍历一遍单链表,求出整个单链表的长度 n,然后将倒数第 k 个,转换为正数第 n-k 个,接下去遍历一次就可以得到结果。但是该方法存在一个问题,即需要对链表进行两次遍历,第一次遍历用于求解单链表的长度,第二次遍历用于查找正数第 n-k 个元素。

显然,以上这种方法还可以进行优化。于是想到了第二种方法,如果沿从头至尾的方向从链表中的某个元素开始,遍历 k 个元素后刚好达到链表尾,那么该元素就是要找的倒数第 k 个元素,根据这一性质,可以设计如下算法:从头结点开始,依次对链表的每一个结点元素进行这样的测试,遍历 k 个元素,查看是否到达链表尾,直到找到那个倒数第 k 个元素。此种方法将对同一批元素进行反复多次的遍历,对于链表中的大部分元素而言,都要遍历 k 个元素,如果链表长度为 n,该算法时间复杂度为 O(kn)级,效率太低。

存在另外一种更高效的方式,只需要一次遍历即可查找到倒数第 k 个元素。由于单链表只能从头到尾依次访问链表的各个结点,因此,如果要找出链表的倒数第 k 个元素的话,也只能从头到尾进行遍历查找,在查找过程中,设置两个指针,让其中一个指针比另一个指针(虽然 Java 语言没有指针的概念,但是引用与指针有着非常相似的性质。为了便于理解,在后续的介绍中都采用指针的概念来介绍)先前移 k-1 步,然后两个指针同时往前移动。循环直到先行的指针值为 NULL 时,另一个指针所指的位置就是所要找的位置。程序代码如下:

    public Node findElemDescK(Node head,int k){
        if(k<1)
            return null;
        Node p1 = head;
        Node p2 = head;
        for(int i =0;i<k-1 && p1!=null;i++)//前移k-1步
            p1 = p1.next;
        if(p1 == null){
            System.out.println("k不合法");
            return null;
        }
        while (p1.next !=null){
            p1 = p1.next;
            p2 = p2.next;
        }
        return p2; //找出单链表中的倒数第 k 个元素
    }

8.1.4 如何实现链表的反转

为了正确地反转一个链表,需要调整指针的指向,而与指针操作相关代码总是非常容易出错的。先举个例子看一下具体的反转过程,例如,i,m,n 是 3 个相邻的结点,假设经过若干步操作,已经把结点 i 之前的指针调整完毕,这些结点的 next 指针都指向前面一个结点。现在遍历到结点 m,当然,需要调整结点的 next 指针,让它指向结点 i,但需要注意的是,一旦调整了指针的指向,链表就断开了,因为已经没有指针指向结点 n,没有办法再遍历到结点 n 了,所以为了避免链表断开,需要在调整 m 的 next 之前要把 n 保存下来。接下来试着找到反转后链表的头结点,不难分析出反转后链表的头结点是原始链表的尾结点,即 next 为空指针的结点。下面给出非递归方法实现链表的反转的实现代码。

    //8.1.4 如何实现链表的反转
    public void ReverseIteratively(Node head){
        Node pReversedHead = head;
        Node pNode = head;
        Node pPrev = null;
        while(pNode !=null){
            Node pNext=pNode.next;
            if(pNext==null)
                pReversedHead=pNode;
            pNode.next=pPrev;
            pPrev=pNode;
            pNode=pNext;
        }
        this.head=pReversedHead;
    }
发布了132 篇原创文章 · 获赞 21 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_40993412/article/details/104526914
今日推荐