个人博客:www.xiaobeigua.icu
之前我们已经使用顺序存储结构实现了线性表,我们会发现虽然顺序表的查询很快,时间复杂度为O(1),但是增删的 效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。这个问题有没有解决方案呢? 有,我们可以 使用另外一种存储结构实现线性表,链式存储结构。
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元 素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成, 结点可以在运行时动态生成。
那我们如何使用链表呢?按照面向对象的思想,我们可以设计一个类,来描述结点这个事物,用一个属性描述这个 结点存储的元素,用来另外一个属性描述这个结点的下一个结点。
结点API设计:
结点类实现:
public class Node<T> { //存储元素 public T item; //指向下一个结点 public Node next; public Node(T item, Node next) { this.item = item; this.next = next; } }
生成链表:
public static void main(String[] args) throws Exception { //构建结点 Node<Integer> first = new Node<Integer>(11, null); Node<Integer> second = new Node<Integer>(13, null); Node<Integer> third = new Node<Integer>(12, null); Node<Integer> fourth = new Node<Integer>(8, null); Node<Integer> fifth = new Node<Integer>(9, null); //生成链表 first.next = second; second.next = third; third.next = fourth; fourth.next = fifth; }
单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据, 指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
//单向列表代码 import java.util.Iterator; public class LinkList<T> implements Iterable<T> { //记录头结点 private Node head; //记录链表的长度 private int N; public LinkList(){ //初始化头结点 head = new Node(null,null); N=0; } //清空链表 public void clear(){ head.next=null; head.item=null; N=0; } //获取链表的长度 public int length(){ return N; } //判断链表是否为空 public boolean isEmpty(){ return N==0; } //获取指定位置i出的元素 public T get(int i){ if (i<0||i>=N){ throw new RuntimeException("位置不合法!"); } Node n = head.next; for (int index = 0; index < i; index++) { n = n.next; } return n.item; } //向链表中添加元素t public void insert(T t){ //找到最后一个节点 Node n = head; while(n.next!=null){ n = n.next; } Node newNode = new Node(t, null); n.next = newNode; //链表长度+1 N++; } //向指定位置i处,添加元素t public void insert(int i,T t){ if (i<0||i>=N){ throw new RuntimeException("位置不合法!"); } //寻找位置i之前的结点 Node pre = head; for (int index = 0; index <=i-1; index++) { pre = pre.next; } //位置i的结点 Node curr = pre.next; //构建新的结点,让新结点指向位置i的结点 Node newNode = new Node(t, curr); //让之前的结点指向新结点 pre.next = newNode; //长度+1 N++; } //删除指定位置i处的元素,并返回被删除的元素 public T remove(int i){ if (i<0 || i>=N){ throw new RuntimeException("位置不合法"); } //寻找i之前的元素 Node pre = head; for (int index = 0; index <=i-1; index++) { pre = pre.next; } //当前i位置的结点 Node curr = pre.next; //前一个结点指向下一个结点,删除当前结点 pre.next = curr.next; //长度-1 N--; return curr.item; } //查找元素t在链表中第一次出现的位置 public int indexOf(T t){ Node n = head; for (int i = 0;n.next!=null;i++){ n = n.next; if (n.item.equals(t)){ return i; } } return -1; } //结点类 private class Node{ //存储数据 T item; //下一个结点 Node next; public Node(T item, Node next) { this.item = item; this.next = next; } } @Override public Iterator iterator() { return new LIterator(); } private class LIterator implements Iterator<T>{ private Node n; public LIterator() { this.n = head; } @Override public boolean hasNext() { return n.next!=null; } @Override public T next() { n = n.next; return n.item; } } }
测试代码:
//测试代码 public class Test { public static void main(String[] args) throws Exception { LinkList<String> list = new LinkList<>(); list.insert(0,"张三"); list.insert(1,"李四"); list.insert(2,"王五"); list.insert(3,"赵六"); //测试length方法 for (String s : list) { System.out.println(s); } System.out.println(list.length()); System.out.println("-------------------"); //测试get方法 System.out.println(list.get(2)); System.out.println("------------------------"); //测试remove方法 String remove = list.remove(1); System.out.println(remove); System.out.println(list.length()); System.out.println("----------------"); for (String s : list) { System.out.println(s); } } }
双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用 来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存 储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。
头结点
按照面向对象的思想,我们需要设计一个类,来描述结点这个事物。由于结点是属于链表的,所以我们把结点类作 为链表类的一个内部类来实现
//双向链表代码 import java.util.Iterator; public class TowWayLinkList<T> { //首结点 private Node head; //最后一个结点 private Node last; //链表的长度 private int N; public TowWayLinkList() { last = null; head = new Node(null,null,null); N=0; } //清空链表 public void clear(){ last=null; head.next=last; head.pre=null; head.item=null; N=0; } //获取链表长度 public int length(){ return N; } //判断链表是否为空 public boolean isEmpty(){ return N==0; } //插入元素t public void insert(T t){ if (last==null){ last = new Node(t,head,null); head.next = last; }else{ Node oldLast = last; Node node = new Node(t, oldLast, null); oldLast.next = node; last = node; } //长度+1 N++; } //向指定位置i处插入元素t public void insert(int i,T t){ if (i<0 || i>=N){ throw new RuntimeException("位置不合法"); } //找到位置i的前一个结点 Node pre = head; for (int index = 0; index < i; index++) { pre = pre.next; } //当前结点 Node curr = pre.next; //构建新结点 Node newNode = new Node(t, pre, curr); curr.pre= newNode; pre.next = newNode; //长度+1 N++; } //获取指定位置i处的元素 public T get(int i){ if (i<0||i>=N){ throw new RuntimeException("位置不合法"); } //寻找当前结点 Node curr = head.next; for (int index = 0; index <i; index++) { curr = curr.next; } return curr.item; } //找到元素t在链表中第一次出现的位置 public int indexOf(T t){ Node n= head; for (int i=0;n.next!=null;i++){ n = n.next; if (n.next.equals(t)){ return i; } } return -1; } //删除位置i处的元素,并返回该元素 public T remove(int i){ if (i<0 || i>=N){ throw new RuntimeException("位置不合法"); } //寻找i位置的前一个元素 Node pre = head; for (int index = 0; index <i ; index++) { pre = pre.next; } //i位置的元素 Node curr = pre.next; //i位置的下一个元素 Node curr_next = curr.next; pre.next = curr_next; curr_next.pre = pre; //长度-1; N--; return curr.item; } //获取第一个元素 public T getFirst(){ if (isEmpty()){ return null; } return head.next.item; } //获取最后一个元素 public T getLast(){ if (isEmpty()){ return null; } return last.item; } //结点类 private class Node{ //存储数据 public T item; //指向上一个结点 public Node pre; //指向下一个结点 public Node next; public Node(T item, Node pre, Node next) { this.item = item; this.pre = pre; this.next = next; } }
测试代码:
//测试代码 public class Test { public static void main(String[] args) throws Exception { TowWayLinkList<String> list = new TowWayLinkList<>(); list.insert("乔峰"); list.insert("虚竹"); list.insert("段誉"); list.insert(1,"鸠摩智"); list.insert(3,"叶二娘"); for (String str : list) { System.out.println(str); } System.out.println("----------------------"); String tow = list.get(2); System.out.println(tow); System.out.println("-------------------------"); String remove = list.remove(3); System.out.println(remove); System.out.println(list.length()); System.out.println("--------------------"); System.out.println(list.getFirst()); System.out.println(list.getLast()); } }
java中LinkedList集合也是使用双向链表实现,并提供了增删改查等相关方法 1.底层是否用双向链表实现; 2.结点类是否有三个域
get(int i):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间 复杂度为O(n)
insert(int i,T t):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的 元素越多,时间复杂度为O(n);
remove(int i):每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元 素越多,时间复杂度为O(n)
相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的, 它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作,,同时它并没有涉及的元素的交换。
相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删 操作比较多,建议使用链表。
单链表的反转,是面试中的一个高频题目。
需求:
原链表中数据为:1->2->3>4
反转后链表中数据为:4->3->2->1
反转API:
public void reverse():对整个链表反转 public Node reverse(Node curr):反转链表中的某个结点curr,并把反转后的curr结点返回
使用递归可以完成反转,递归反转其实就是从原链表的第一个存数据的结点开始,依次递归调用反转每一个结点, 直到把最后一个结点反转完毕,整个链表就反转完毕。
代码:
public void reverse(){ if (N==0){ //当前是空链表,不需要反转 return; } reverse(head.next); } /** * * @param curr 当前遍历的结点 * @return 反转后当前结点上一个结点 */ public Node reverse(Node curr){ //已经到了最后一个元素 if (curr.next==null){ //反转后,头结点应该指向原链表中的最后一个元素 head.next=curr; return curr; } //当前结点的上一个结点 Node pre = reverse(curr.next); pre.next = curr; //当前结点的下一个结点设为null curr.next=null; //返回当前结点 return curr; }
测试代码:
//测试代码 public class Test { public static void main(String[] args) throws Exception { LinkList<Integer> list = new LinkList<>(); list.insert(1); list.insert(2); list.insert(3); list.insert(4); for (Integer i : list) { System.out.print(i+" "); } System.out.println(); System.out.println("--------------------"); list.reverse(); for (Integer i : list) { System.out.print(i+" "); } } }