알고리즘 검토 (11) : 기본 데이터 구조의 연결 목록으로 돌아 가기

1. 전면에 쓰기

이 기사는 연결 목록에 대한 리뷰입니다. 여기서 연결 목록의 작동은 일반적으로 포인터를 사용하는 것입니다. 이중 포인터와 세 개의 포인터가 함께 사용됩니다. 빠르고 느린 포인터는 관습을 깨뜨립니다. 여기서 문제를 해결하는 열쇠는 다음과 같습니다. 먼저 그림을 그린 다음 해당 포인터를 찾아 변형 작업을 수행합니다. 버그가 발생하기 쉬운 곳은 범위를 벗어 났거나 포인터가 무한 루프로 이동하는 것을 잊었습니다. 다음은 연결 목록과 관련된 몇 가지 일상적인 작업을 분류 한 다음 특정 질문과 코드를 분류하고 마지막으로 질문 만 분류하여 나중에 사용할 수 있도록하는 것입니다. 아래에서 시작하십시오. 기본 지식 참조

연결 목록에 대해 우리는 지식 포인트를 알아야합니다.

  1. 일반적인 작업 : 요소 추가, 삭제, 수정 및 확인, 더 복잡한 작업은 모든 종류의 포인터입니다.

  2. 링크 목록 액세스 배열 시간 복잡도 O (n), 삽입 및 삭제 시간 복잡도 O (1), 메모리가 연속적이지 않음

  3. 보다 고전적인 주제 중 일부는 연결 목록사전 보간, 사후 보간, 역순, 뒤집기, 병합 및 이동 을 검토합니다 .

  4. 여기에 관련된 몇 가지 아이디어 :

    • 헤드 노드 를 선언하는 헤드 노드 아이디어 는 많은 일에 편리 할 수 ​​있습니다. 일반적으로 새로운 링크드리스트를 반환하는 문제에 사용됩니다. 예를 들어, 두 개의 순서가있는 링크드리스트가 주어지면 그것들은 순서대로 통합되고 정렬되어야합니다. 또 다른 예로, 연결 목록의 홀수와 짝수가 원래 순서로 분리 된 다음 새로운 연결 목록으로 재결합되면 연결 목록의 첫 번째 절반은 홀수이고 두 번째 절반은 짝수입니다.
    • 머리 반전 생각
    • 이중 포인터 탐색 , 쌍대 스왑, 역순, 뒤집기 등과 같은 많은 작업을 수행 할 수 있습니다.
    • 빠르고 느린 포인터 의 아이디어는 일반적으로 링에서 사용할 수 있습니다.
    • 재귀, 연결 목록의 주제는 재귀하기 쉽습니다.
  5. 장점과 단점:

    1. 장점 : 연결 목록은 메모리 공간을 유연하게 할당 할 수 있으며 O (1) O (1)에 있을 수 있습니다.요소의 이전 요소가 알려진 경우 O ( 1 ) 시간 내에 요소를 삭제하거나 추가합니다. 물론 단일 연결 목록인지 이중 연결 목록인지에 따라 달라집니다. , 요소의 다음 요소가 알려진 경우O (1) O (1) 일수도 있습니다.O ( 1 ) 시간 내에 요소를 삭제하거나 추가하십시오.
    2. 단점 : 배열과 달리 첨자 요소를 빠르게 읽을 수 있으며, 링크 된 목록의 헤드에서 시작할 때마다 읽을 수 있습니다. 첫 번째 쿼리 kkk 요소는O (k) O (k) 필요O ( k ) 시간.
  6. 응용 시나리오 : 해결해야 할 문제에 많은 빠른 쿼리가 필요한 경우 연결된 목록이 적합하지 않을 수 있습니다. 문제가 발생하면 데이터 요소 수가 불확실하고 데이터를 자주 추가 및 삭제해야하는 경우 연결된 목록이 적합하지 않을 수 있습니다. 목록이 더 적합 할 것입니다. 그리고 데이터 요소의 크기가 결정되고 삭제 및 삽입 작업이 많지 않으면 배열이 더 적합 할 수 있습니다.

  7. 제안 : 연결 목록의 문제를 해결할 때 종이나 화이트 보드에 노드 간의 관계를 그린 다음 수정 방법을 그려 문제를 분석하고 면접 중에 면접관이 명확하게 할 수 있도록 할 수 있습니다. 당신의 생각을 참조하십시오.

연결 목록과 관련하여 먼저 조회, 삽입 및 삭제 작업과 같은 여러 루틴을 마스터해야합니다.

파이썬에서 연결 목록 노드를 정의하는 방법을 알아야합니다. 일반적으로이 부분을 LeetCode에서 작성할 필요는 없지만 실제 인터뷰에서는 제공하지 않고 직접 작성해야합니다.

class ListNode:
	def __init__(self, val=0, next=None):
		self.val = val
		self.next = next

다음은 연결 목록의 일반적인 작업이며 직접 질문을 정리하여 구성합니다.

이 문제는 먼저 위의 코드 인 링크드리스트 노드를 설정 한 다음 헤드 노드를 초기화해야합니다.

class MyLinkedList:
    def __init__(self):
        """
        Initialize your data structure here.
        """
        self.dummyHead = ListNode(-1)

연결된 목록의 조회 작업은 색인이 주어지면 해당 색인의 값을 반환합니다.

def get(self, index: int) -> int:
    """
    Get the value of the index-th node in the linked list. If the index is invalid, return -1.
    """
    if index < 0: return -1
    p = self.dummyHead.next
    cou = 0
    while p and cou < index:
        p = p.next
        cou += 1
    # self.printl()   打印当前链表结果
    return p.val if p else -1

연결 목록의 첫 번째 위치에 삽입 :

def addAtHead(self, val: int) -> None:
    """
    Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list.
    """
    node = ListNode(val)
    node.next = self.dummyHead.next
    self.dummyHead.next = node
    # self.printl()   # 打印当前链表结果

연결 목록 끝에 삽입 :

def addAtTail(self, val: int) -> None:
    """
    Append a node of value val to the last element of the linked list.
    """
    node = ListNode(val)
    # 首先到尾部
    pre, cur = self.dummyHead, self.dummyHead.next
    while cur:
        pre, cur = cur, cur.next
    pre.next = node
    # self.printl()   # 打印当前链表结果

연결된 목록의 지정된 위치에 삽입 :

def addAtIndex(self, index: int, val: int) -> None:
    """
    Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted.
    """
    node = ListNode(val)
    cou = 0
    pre, cur = self.dummyHead, self.dummyHead.next
    while cur and cou < index:
        pre, cur = cur, cur.next
        cou += 1
    # 插入
    node.next = cur
    pre.next = node
    # self.printl()   打印当前链表结果

연결 목록 작업 삭제 :

def deleteAtIndex(self, index: int) -> None:
    """
    Delete the index-th node in the linked list, if the index is valid.
    """
    cou = 0
    pre, cur = self.dummyHead, self.dummyHead.next
    while cur and cou < index:
        pre, cur = cur, cur.next
        cou += 1
    # 删除
    pre.next = cur.next if cur else None
    # self.printl()   打印当前链表结果

디버깅을 위해 링크드리스트의 요소를 출력하는 함수도 썼는데, 처음 제출했을 때 오류가 있었고 어떤 단계가 문제인지 몰라서 다음과 같은 함수를 작성했습니다. 이 작업의 각 단계 후에 출력하고 오류가 즉시 나타납니다.

def printl(self):
    p = self.dummyHead.next
    while p:
        print(p.val, end=',')
        p = p.next
    print()

기본 코드와 관련하여 두 가지 추가 작업, 즉 연결 목록을 작성하기위한 전방 삽입과 꼬리 삽입이 있는데, 이는 특정 주제에 대한 이해에서 충족 될 것입니다. 전면 보간은 역방향 연결 목록을 가져오고 꼬리 보간은 일반 연결 목록을 가져옵니다.

아래의 특정 주제를보십시오.

2. 질문 아이디어 및 코드 정렬

2.1 연결 목록 요소 삭제

  • LeetCode203 : 연결 목록 요소 제거 : 연결 목록 작업의 가장 기본적인 문제는 연결 목록의 삭제를 검사하는 것입니다. 이때 pre와 cur 두 포인터가 순회됩니다. Pre는 cur의 이전 노드를 가리키는 역할을합니다. 찾은 후 요소를 삭제하고 cur은 대상 요소를 찾는 책임이 있습니다. 헤드 노드를 선언하면 작업이 더 쉬워집니다.
    여기에 사진 설명 삽입
  • LeetCode19 : 연결리스트의 K 번째 노드 삭제 이 요구가 사용하기 : 빠르고 느린 포인터를fast, slow , LET fast이동 N이 때 먼저 다음 빠른 단계와 손에 천천히 이동 손, fast마지막에 온다, 그것은 slow로부터 n 번째 노드를 bottom 이지만 여기서는 하단 Node에서 n 번째 노드를 삭제하고 싶으므로 진행하는 동안 느린 레코드 앞에 노드가 있어야합니다. 첫 번째와 중간의 삭제를 통합하려면 여기에 헤드 노드가 필요합니다.
    여기에 사진 설명 삽입

2.2 연결 목록 반전

  • LeetCode206 : Reverse Linked List : 세 가지 방법이 있으며, 모두 매우 기본적인 것입니다. 여기에서 정리하고 싶습니다.

    1. 우선, pre, cur이 두 친구는 정말 사용하기 쉽습니다. 앞으로 순회 할 때 포인터가 조정됩니다. 파이썬에서 포인터 조정을위한 코드는 더 간결하게 튜플 풀기를 사용할 수 있지만 이것을 작성하기 전에 다음을 수행하는 것이 가장 좋습니다. 프로토 타입을 작성하지 않으면 실수하기 쉽습니다.
      여기에 사진 설명 삽입
      사실 프로토 타입에 따라 위의 코드를 작성하는 것은 매우 간단합니다. 프로토 타입의 등호 왼쪽은 순서대로 왼쪽에 배치되고 오른쪽에는 프로토 타입의 등호 측면이 순서대로 오른쪽에 배치됩니다. 그러나 프로토 타입은 올바르게 작성되어야하며 이는 조작 순서와 관련이 있습니다.
    2. 헤드 보간으로 배열을 다시 만드는 아이디어 : 위에서 언급했듯이 헤드 보간은 역순으로 연결된 목록을 만들 수 있습니다.
      여기에 사진 설명 삽입
    3. 재귀 적 사고 :이 질문은 꼬리 재귀적일 수 있습니다. 현재 들어오는 노드의 경우 뒤에있는 노드의 역순, 즉 head->next모든 역순이있는 경우 머리를 끝에 연결하기 만하면됩니까? ? 그냥 직시하세요.이 아이디어를 바탕으로 꼬리 재귀를 사용할 수 있습니다.

      여기에 사진 설명 삽입
  • LeetCode25 : K a group of flipped linked lists :이 질문 은 연결 목록을 뒤집는 아이디어를 사용해야하지만 여기에서는 모두를 뒤집는 것이 아니라 한 그룹을 뒤집는 것이므로 먼저 위의 연결 목록 반전을 함수로 작성하십시오. Node.js에서 반전을 실현하십시오 [a, b). 그런 다음 되돌릴 수 있으며 여기에 한 번에 K 노드를 삽입하여 연결 목록을 다시 작성하는 아이디어가 있습니다. 두 개의 포인터를 사용 k_start, k_end, k_end이동 KK 처음k 단계를 수행 한 다음[k_start, k_end)노드를 반전하고 반전 된 노드의 꼬리를 새 연결 목록에 삽입합니다 (꼬리 삽입 방법). 후자는 충분하지 않다고 발견되면 그 다음, 위의 과정을 계속 수행 할KK가k가 있으면 나머지 직접 꼬리를 새 연결 목록에 삽입합니다. 여기서 핵심은 주어진 위치에서 링크 목록 노드와 꼬리 삽입 아이디어를 뒤집는 것입니다.
    여기에 사진 설명 삽입

  • LeetCode61 : Rotating linked list : 이전에 회전 배열을 사용해 보았습니다 . 여기에 연결된 목록의 선택이 있습니다. 아이디어는 실제로 배열의 아이디어와 동일합니다. 먼저 전체 순서를 반대로 한 다음 첫 번째 k를 반대로 한 다음 반대로 그 뒤에 k. 그 당시에는 링크드리스트의 동작이 그렇게 간단 할 수 없었습니다. 순서가 역전 될 때 위의 역기능이 매우 중요합니다. 여전히 꼬리 보간 아이디어 + 역 간격 노드 동작입니다.
    여기에 사진 설명 삽입

2.3 링크드리스트 노드 교환

  • 연결된 목록의 노드를 쌍 으로 교환 :이 주제는 다시 교환되며 교환은 역순과 비슷하지만 더 간단합니다. 인접 노드의 역순, 이중 포인터 + 꼬리 삽입의 아이디어 연결 목록을 작성합니다 .
    여기에 사진 설명 삽입

2.4 순환 연결 목록

  • 순환 연결 목록 : 연결 목록 에서 링을 찾는 가장 좋은 도구는 속도 포인터입니다. 두 개의 포인터를 정의 fast, slow합니다. slow한 단계 fast아래로 두 단계 아래로 이동할 때마다 특정 순간에 만나면 링이 있음을 의미합니다.

    여기에 사진 설명 삽입
  • 순환 연결 목록 II :이 질문은 아이디어에 직접적으로 있습니다. 링의 입구를 찾으려면 먼저 만남의 지점을 찾으십시오. 그 후 시작 지점으로 천천히 돌아가고 만남의 지점에 빠르게 머물러 있습니다. 그들이 다시 만날 때 반지의 입구입니다. 왜? 처음에 이것은 거리 계산 문제입니다. 처음 만났을 때 느리게 K 걸음, 빠른 속도가 2K 걸음 (속도가 두 배 느림)을 밟았다 고 가정하면 시작 지점에서 천천히 걷는 것과 같습니다. 만남 지점의 거리는 만남의 지점에서 단식의 거리와 같으며, 원을 만남의 지점으로 되돌리고 , 출발 지점은 만남 지점 이전에 지나지 않습니다. 만남의 지점에서 출발점까지, 머리는 출발 지점에서 멀어지며, 이는 만나는 지점에서 출발 지점까지의 거리 (Km 걸음)와 동일합니다. 그래서 두 사람이 먼저 만나고 나서 시작점으로 천천히 돌아간 다음 동시에 걸을 때 만나는 것이 시작점입니다.

여기에 사진 설명 삽입

물론 위에서 발견 된 두 개의 루프는 해시 테이블을 사용하여 쉽게 제거 할 수 있습니다. 두 번째 루프 찾기 항목을 가져 오면 링크 된 목록을 순회하는 데 포인터가 하나만 필요합니다. 노드가 순회 될 때마다 저장됩니다. 집합 컬렉션. 특정 노드가 순회 된 것으로 확인되면 이미 해시 테이블에이 노드가있어 이것이 링의 엔트리임을 나타내며이 노드로 돌아갑니다. 마찬가지로 링이 있다고 판단 할 수도 있습니다. 현재.
여기에 사진 설명 삽입

  • LeetCode876 : 연결 목록의 중간 노드 : 빠른 포인터와 느린 포인터의 고전적인 문제입니다. 둘 다 처음부터 동시에 시작 합니다. Fast는 한 번에 두 단계 씩, 느린 것은 한 번에 한 단계 씩 걸립니다. 끝, 느린 것은이 시점에서 중간 노드입니다.
    여기에 사진 설명 삽입

2.5 연결된 목록 병합

3. 장군님

링크드리스트에 대한 질문이 비교적 적기 때문에 하루를 들여 다시 검토했습니다. 몇 가지 기본 아이디어가 매우 중요하고 그림도 매우 중요합니다. 링크드리스트에서 자주 사용하는 표준 구성은 다음과 같습니다.

  1. 헤드 노드dummyHead
  2. pre, cur두 가지 포인터
  3. fast, slow두 가지 포인터
  4. 테일 플러그 + 이중 포인터, 테일 플러그 + 헤드 플러그

이제 더블 포인터의 표준 구성 :
5. pre, cur두 개의 포인터 (앞뒤 포인터 —> 연결 목록 노드의 삽입, 삭제 및 반전)
6. fast, slow두 포인터 (빠른 포인터와 느린 포인터 —> 링을 판단하고 중간 지점을 찾습니다. 연결 목록, 하단 Node에서 N 번째 찾기)
7. left, right두 개의 포인터 (왼쪽 및 오른쪽 포인터-> 배열, 문자열 관련 항목)
8. win_start, win_end두 개의 포인터 (슬라이딩 창-> 하위 배열, 하위 문자열 관련 항목)

이 글의 중요한 생각 :

  • 테일 플러그 + 이중 포인터
  • 꼬리 삽입 + 역 간격 링크 목록 노드
  • 테일 플러그 + 헤드 플러그

마지막 주제는 다음과 같이 요약됩니다.

연결 목록 요소 삭제 :

연결 목록 반전

링크드리스트 노드 교환

순환 연결 목록

연결된 목록 병합

추천

출처blog.csdn.net/wuzhongqiang/article/details/115266111