1,顺序表
逻辑上相邻的两个元素物理位置上也相邻。
可以随机读取,但增删操作复杂。
2,单链表
1)概念
读取麻烦,增删简单。
头指针和尾指针无法决定链表长度。
2)定义
结点定义:
struct ListNode{
int value;
ListNode* next;
};
为链表末尾添加一个结点:
//C、C++代码
void addToTail(ListNode** pHead, int value){
ListNode* pNew = new ListNode();
pNew -> value = value;
pNew -> next = NULL;
if(*pHead == NULL){//空表
*pHead = pNew;
}else{
ListNode* pNode = *pHead;
while(pNode -> next != NULL)//遍历找到尾指针
pNode = pNode->next;
pNode -> next = pNew;
}
}
//java代码
class ListNode{
int val;
ListNode next;
}
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<>();
if (listNode == null)
return list;
Stack<ListNode> stack = new Stack<>();
while (listNode != null) {
stack.push(listNode);
listNode = listNode.next;
}
while (!stack.isEmpty()) {
list.add(stack.pop().val);
}
return list;
}
@Test
public void test() {
ListNode listNode0 = new ListNode();
ListNode listNode1 = new ListNode();
ListNode listNode2 = new ListNode();
ListNode listNode3 = new ListNode();
ListNode listNode4 = new ListNode();
ListNode listNode5 = new ListNode();
listNode0.val = 0;
listNode0.next = listNode1;
listNode1.val = 1;
listNode1.next = listNode2;
listNode2.val = 2;
listNode2.next = listNode3;
listNode3.val = 3;
listNode3.next = listNode4;
listNode4.val = 4;
listNode4.next = listNode5;
listNode5.val = 5;
listNode5.next = null;
System.out.print(printListFromTailToHead(listNode0));
}
3)应用
①从尾到头打印链表
输入一个链表,从尾到头打印链表每个节点的值。
解题思路:由于打印是只读操作,不宜改变原数据格式。可以借助辅助栈,或者使用递归实现(注意链表太长会导致函数调用层级变深,溢出)。
②在O(1)时间删除链表结点
思路:要删除结点i
,将i
的下一个结点j
的内容复制到i
,然后把i
的指针指向结点j
的下一个结点。这样删除结点i,不需要知道i
的前一个结点。(O(1))
若删除的结点位于链表的尾部,那么需要从头结点开始遍历,得到前序结点再删除。(O(n))
若链表只有一个结点且需删除,那么还需要在删除后把头结点置NULL。
总的复杂度:[(n-1)*O(1) +O(n)]/n = O(1)
public class ListNode{
ListNode nextNode;
int data;
}
public void deleteNode(ListNode head, ListNode deListNode) {
if (deListNode == null || head == null)//链表为null
return;
if (head == deListNode) {//链表只有一个结点
head = null;
} else {
if (deListNode.nextNode == null) {// 若删除节点是末尾节点,往后移一个
ListNode pointListNode = head;
while (pointListNode.nextNode.nextNode != null) {
pointListNode = pointListNode.nextNode;
}
pointListNode.nextNode = null;
} else {//O(1)删除一个结点
deListNode.data = deListNode.nextNode.data;
deListNode.nextNode = deListNode.nextNode.nextNode;
}
}
}
④链表中倒数第k个结点
输入一个链表,输出该链表中倒数第k个结点。
思路:定义一快一慢两个指针,快指针走K步,然后慢指针开始走,快指针到尾时,慢指针就找到了倒数第K个节点。
类似题目:
i>找中间节点:
使用两个指针,同时从头结点出发,一个走一步,一个走两步。
当走的快的结点到链表尾部时,走得慢的指针正好在链表中间。
如果链表中结点为奇数个,返回中间结点;如果是偶数,返回中间结点中的任意一个。
ii>判断一个单向链表是否 形成了环形结构:
使用两个指针,同时从头结点出发,一个走一步,一个走两步。
如果走的快的指针追上了走得慢的指针,那么就是环形链表。
如果走的快的指针走到链表的末尾都没有追上第一个指针,那么就不是环形。
public class ListNode{
ListNode next;
int data;
}
public ListNode FindKthToTail(ListNode head,int k) {
if (head == null || k <= 0) {
return null;
}
ListNode fast = head;
ListNode slow = head;
while(k-- > 1) {
if (fast.next != null)
fast = fast.next;
else
return null;
}
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
⑤反转链表
输入一个链表,反转链表后,输出链表的所有元素。
难点:可能出现断链。
class ListNode{
ListNode next;
int val;
}
public ListNode ReverseList(ListNode head) {
if (head == null) {
return null;
}
ListNode pPrev = null;
while(head != null) {//head保存当前结点
ListNode pNext = head.next;//保存当前结点的后一个结点
head.next = pPrev;//翻转上一次的两个结点
pPrev = head;//保存当前结点,是下一次循环的前一个结点
head = pNext;//head指针后移,处理下一个结点
}
return pPrev;
}
⑥链表合并
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
思路:判断两个头结点的值,用较小的作为新链表的头结点。
public class ListNode{
ListNode next;
int val;
}
public ListNode Merge(ListNode list1,ListNode list2) {
if (list1 == null) {
return list2;
}
if (list2 == null) {
return list1;
}
ListNode newHead = null;
if (list1.val <= list2.val) {
newHead = list1;
newHead.next = Merge(list1.next,list2);
}else {
newHead = list2;
newHead.next = Merge(list1,list2.next);
}
return newHead;
}
⑦复杂链表的复制
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
解题思路:用辅助空间O(n)来存储复制的链表。
1)根据原始链表,对每一个结点N创建它的复制结点N’,并插入到N之后。
2)复制random指针:对原始链表每一个random指针N->S,有N’->S’。
3)拆分为两个链表:奇数位为原始链表,偶数位为复制链表。
代码如下:
public class RandomListNode {
int label;
RandomListNode next = null;
RandomListNode random = null;
RandomListNode(int label) {
this.label = label;
}
}
public RandomListNode Clone(RandomListNode pHead) {
if (pHead == null)
return null;
RandomListNode head = new RandomListNode(pHead.label);
RandomListNode temp = head;
while (pHead.next != null) {
temp.next = new RandomListNode(pHead.next.label);
if (pHead.random != null) {
temp.random = new RandomListNode(pHead.random.label);
}
pHead = pHead.next;
temp = temp.next;
}
return head;
}
⑧求两个单链表的第一个公共结点
输入两个链表,找出它们的第一个公共结点。
思路一:两个链表有公共结点,那么第一个公共结点之后的结点都是重合的。如下图:
对于两个链表,如果有公共结点,那么公共结点一定出现在尾部。
从尾部开始比较结点,最后一个相同的结点就是第一个公共结点。
为了实现这种后进先出,使用辅助栈来分别存储这两个单链表。
这种方法虽然可以实现,但是以空间换时间,空间复杂度大。
思路二:先求出链表长度,然后长的链表先走多出的几步,然后两个链表同时向下走去寻找相同的结点。
代码如下:需要遍历两遍,第一遍是为了同步指针。第二遍是保持两个指针同步遍历寻找相同的结点。
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode p1 = pHead1;
ListNode p2 = pHead2;
while (p1 != p2) {
p1 = (p1 != null ? p1.next : pHead2);
p2 = (p2 != null ? p2.next : pHead1);
}
return p1;
}
⑨
3)循环链表
表中最后一个结点的指针指向头结点,只有头结点是固定的。
4)双向链表
data *prior(直接前驱) *next(直接后继)
①二叉搜索树和双向链表的转换
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
解题思路:
树的每个结点有2个指针指向它的左右子结点,调整这两个指针。
二叉搜索树的顺序:左子树总数小于父结点,右子树总是大于父结点。
所以调整方案为:原先指向左子树的指针为指向链表中前一个结点的指针,指向右子树的结点为指向链表中后一个结点的指针。这是一个递归的过程。
转换方案:定义一个链表的尾指针,递归处理左右子树,最后返回链表的头节点。
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
public TreeNode Convert(TreeNode pRootOfTree) {
TreeNode lastlist = covertNode(pRootOfTree, null);
TreeNode pHead = lastlist;
while (pHead != null && pHead.left != null) {
pHead = pHead.left;
}
return pHead;
}
public TreeNode covertNode(TreeNode root, TreeNode lastlist) {
if (root == null)
return null;
TreeNode cur = root;
if (cur.left != null) {
lastlist = covertNode(cur.left, lastlist);
}
cur.left = lastlist;
if (lastlist != null) {
lastlist.right = cur;
}
lastlist = cur;
if (cur.right != null) {
lastlist = covertNode(cur.right, lastlist);
}
return lastlist;
}