一、 什么是链表?
链表是一种存储数据集合的数据结构。有以下特性:
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
问题三:约瑟夫环,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