数据结构——链表
链表(Linked List)
一种线性数据结构,其中的每个元素都是一个节点对象。各个节点通过“引用”(指针)相连接,引用中记录了下一个节点的内存地址,通过其可以定位并访问到下一个节点。
链表对比数组有更好的灵活性,数组要求内存空间是连续的,但当数组非常庞大时,可能无法提供那么大的连续空间,同时数据量庞大的数组除了对其进行查找操作外,其余操作都非常耗费资源,这时链表的灵活性就体现出来了。
如图所示,链表的结构如图。故而在相同数据量下,链表比数组要占据更多的内存空间,而链表的增删操作在时间复杂度O(1),数组为O(n)。这是典型的空间换时间的设计。
对于链表的基本操作如下(单链表介绍)
1 class ListNode { 2 int val; 3 ListNode next; 4 ListNode(int x){ 5 val = x; 6 } 7 } 8 public class LinkList { 9 public static void main(String[] args){ 10 11 Scanner in = new Scanner(System.in); 12 //初始化链表 13 ListNode n0 = new ListNode(1);//头节点,整个链表的代称,该链表可记作n0 14 ListNode n1 = new ListNode(3); 15 ListNode n2 = new ListNode(2); 16 ListNode n3 = new ListNode(4); 17 ListNode n4 = new ListNode(5); 18 n0.next = n1; 19 n1.next = n2; 20 n2.next = n3; 21 n3.next = n4; 22 23 ListNode HeadNode = n0; 24 //遍历链表 25 showList(HeadNode); 26 27 //插入节点 28 HeadNode = insert(HeadNode,1,new ListNode(6)); 29 30 showList(HeadNode); 31 32 // HeadNode = deleteList(HeadNode,3); 33 // showList(HeadNode); 34 // 35 // int target = in.nextInt(); 36 // System.out.println("值为"+target+"的索引为"+findNode(HeadNode,target)); 37 38 alterList(HeadNode,3,7); 39 showList(HeadNode); 40 41 } 42 43 //指定位置插入节点 44 public static ListNode insert(ListNode headnode,int n,ListNode p){ 45 ListNode temp = headnode; 46 if ( n == 1) { 47 p.next = headnode; 48 headnode = p; 49 return headnode; 50 } 51 for (int i = 1; i < n-1; i++) { 52 if (temp.next == null){ 53 temp.next = p; 54 } 55 temp = temp.next; 56 } 57 p.next = temp.next; 58 temp.next = p; 59 return headnode; 60 } 61 62 public static void showList(ListNode headnode){ 63 for (ListNode node=headnode; node != null ; node = node.next) { 64 if (node.next == null){ 65 System.out.print(node.val+"\n"); 66 }else 67 System.out.print(node.val+"->"); 68 } 69 } 70 71 public static ListNode deleteList(ListNode headnode,int n){ 72 ListNode temp = headnode; 73 if (n == 1) { 74 headnode = headnode.next; 75 return headnode; 76 } 77 for (int i = 1; i < n-1; i++) { 78 temp = temp.next; 79 } 80 temp.next = temp.next.next; 81 return headnode; 82 } 83 84 public static int findNode(ListNode headnode,int target){ 85 int index = 1; 86 while(headnode!=null){ 87 if (headnode.val == target) 88 return index; 89 headnode = headnode.next; 90 index++; 91 } 92 return index; 93 } 94 95 public static void alterList(ListNode HeadNode,int index,int n){ 96 for (int i = 1; i < index ; i++) { 97 HeadNode = HeadNode.next; 98 } 99 HeadNode.val = n; 100 } 101 }
其实对于链表的操作而言,难点在于“节点”的定位,所有函数中都使用临时指针来进行操作,可以把其当成数组的索引(下标)。
比如在插入节点操作中,其难点就在于要定位至要插入位置的前一个节点,也就是说要临时指针指向插入位置的前一个节点。
还有一个小问题我纠结了好久,就是在初学时不同资料在遍历定位时使用HeadNode或者HeadNode.next,其实这很无所谓,只要搞明白你要使用什么去操作,习惯用节点就使用HeadNode,习惯放弃头节点而专注于HeadNode.next就用HeadNode.next。
对于环形链表和双向链表而言,其操作的核心思想和单链表大同小异。在环形链表中,难点除了上面提到的,便是对于“头节点”的定位,环形链表中,为了不迷路,方便操作,最好使用一个指针指定一个节点作为“头节点”,也就是要指定一个方向标!而双向链表更加简单,仅仅只是在定义节点时要加上前驱指针,同时操作时也要考虑前驱节点。