{ }codelper
글자 번호문자열 잇기P0144

웹 주소 경로 정리하기

첫 줄에 주소 수 n이 주어지고, 이어서 웹 주소의 경로 부분이 한 줄에 하나씩 n개 주어진다. 주소마다 경로를 정리해 한 줄에 하나씩 출력하라. 주소에 # 가 있으면 처음 나오는 # 부터 끝까지가 조각이다. 조각을 뺀 앞부분에 ?
가 있으면 처음 나오는 ?
부터 그 앞부분의 끝까지가 질의다. 나머지 맨 앞부분이 경로다. 그래서 조각 안의 ?
는 질의가 아니고, 질의와 조각 안의 / 와 .
은 경로가 아니다. 경로를 / 가 있는 자리마다 끊어 나눈 부분 하나하나를 이름이라 하고, 앞에서부터 차례로 다음처럼 처리한다. 빈 이름("//"나 맨 앞·맨 끝의 / 때문에 생긴 것)과 "."은 버린다. ".."은 지금까지 남은 이름 중 마지막 하나를 지운다.
남은 이름이 없으면 아무 일도 하지 않는다. 그 밖의 이름("...", "..a", ".git" 포함)은 그대로 남긴다. 남은 이름들을 / 로 이어 맨 앞에 / 를 붙인 것이 정리한 경로다(남은 이름이 없으면 "/"). 경로의 마지막 이름(맨 끝 / 뒤의 부분)이 빈 이름, ".", ".." 중 하나이면 그 주소는 폴더를 가리킨다.
이때 남은 이름이 하나 이상이면 정리한 경로 끝에 / 를 하나 붙인다. 정리한 경로 뒤에 질의와 조각을 원래 그대로 붙여 출력한다.

입력
1 ≤ n ≤ 100.
주소는 / 로 시작하고 길이 1 ~ 200이며, 영어 소문자, 숫자, 그리고 .
- _ / ?
# = & 로만 이루어진다
출력
정리한 주소 n줄
예시 1 · stdin
4
/a/./b/../c/
/../x//y
/docs/v1/../v2/index.html?lang=ko#top
/a/b?next=/../c#x?y
stdout
/a/c/
/x/y
/docs/v2/index.html?lang=ko#top
/a/b?next=/../c#x?y
예시 2 · stdin
5
/a/b#x/../y?z
/./.../..a/.
/a/b/..
/a/..
/
stdout
/a/b#x/../y?z
/.../..a/
/a/
/
/
main.py
입력(stdin)

실행하면 이 줄들이 차례로 input()·표준 입력으로 들어가요(실행 중에 묻지 않아요).

$ 준비됐어요

코드를 입력해 풀려면 PC에서 열어 주세요. 모바일에서는 문제와 풀이를 볼 수 있어요.

다른 언어로는?언어별 풀이 보기 · 8개 언어 풀이

함정이 다섯이에요.
첫째, ?를 문자열 전체에서 먼저 찾으면 "/a/b#x/../y?z"에서 조각 안의 ?를 질의의 시작으로 보고, 조각의 x/../y까지 경로로 정리해 버려요.
#를 먼저 찾고, ?는 그 앞부분에서만 찾아야 해요.
처음 나오는 자리 찾기는 Python find(없으면 -1, index는 예외), JS·TS·Java indexOf, C# IndexOf, C strchr(없으면 NULL), C++ find(없으면 npos), 크레이콘 자리찾기(없으면 -1)예요.
둘째, 질의·조각을 떼지 않고 통째로 나누면 "?next=/../c"의 ..까지 경로로 처리해요.
셋째, 맨 위에서 ..을 만나는 경우예요.
Python list.pop()은 빈 목록에서 IndexError, Java remove(size() - 1)은 IndexOutOfBoundsException, C++ 빈 vector의 pop_back()은 정의되지 않은 동작이라 먼저 비었는지 물어야 해요.
JS·TS pop()은 빈 배열에서 그냥 undefined를 돌려줘서 묻지 않아도 돼요.
넷째, 점으로 시작하는지만 보면 "..."이나 ".git"도 지워요.
이름 전체가 .이나 ..과 같은지 봐야 해요.
다섯째, 끝의 /를 늘 떼면 "/a/c/"가 "/a/c"가 돼요.
경로가 /로 끝나는지만 봐도 모자라요.
"/a/b/.."처럼 .이나 ..으로 끝나도 폴더를 가리키니 "/a/"예요.
나누기는 언어마다 빈 이름을 다르게 다뤄요.
Python split("/"), JS·TS split("/"), 크레이콘 나누기("/")는 빈 이름을 남기고, Java split("/")은 끝의 빈 이름만 버리며, C# Split('/', StringSplitOptions.RemoveEmptyEntries)와 C strtok은 모두 버려요.
그래서 Java·C#·C에서는 나눈 결과의 끝을 보면 "/a/"의 빈 이름을 놓쳐요.
마지막 이름은 마지막 / 뒤를 잘라서 봐요(lastIndexOf('/'), C strrchr, C++ rfind).
이어 붙이기는 Python "/".join(stack), JS·TS stack.join("/"), Java String.join("/", stack), C# string.Join("/", stack), 크레이콘 stack.잇기("/")이고, C·C++에는 없어서 첫 이름 앞만 빼고 /를 붙이며 이어 써요.

Pythonmain.py · 26 줄
def normalize(url):    # 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다    h = url.find("#")    rest, frag = (url, "") if h == -1 else (url[:h], url[h:])    q = rest.find("?")    path, query = (rest, "") if q == -1 else (rest[:q], rest[q:])    segs = path.split("/")    stack = []    for seg in segs:        if seg == "" or seg == ".":            continue        if seg == "..":            if stack:  # 맨 위에서는 더 올라가지 않는다                stack.pop()        else:            stack.append(seg)    clean = "/" + "/".join(stack)    # 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다    if stack and segs[-1] in ("", ".", ".."):        clean += "/"    return clean + query + frag  n = int(input())for _ in range(n):    print(normalize(input()))
JavaScriptmain.js · 25 줄
const lines = require("fs").readFileSync(0, "utf8").split("\n");const n = Number(lines[0]); function normalize(url) {  // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다  const h = url.indexOf("#");  const rest = h === -1 ? url : url.slice(0, h);  const frag = h === -1 ? "" : url.slice(h);  const q = rest.indexOf("?");  const path = q === -1 ? rest : rest.slice(0, q);  const query = q === -1 ? "" : rest.slice(q);  const stack = [];  for (const seg of path.split("/")) {    if (seg === "" || seg === ".") continue;    if (seg === "..") stack.pop(); // 빈 배열의 pop()은 오류 없이 undefined    else stack.push(seg);  }  let clean = "/" + stack.join("/");  // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다  const last = path.slice(path.lastIndexOf("/") + 1);  if (stack.length > 0 && (last === "" || last === "." || last === "..")) clean += "/";  return clean + query + frag;} console.log(lines.slice(1, 1 + n).map(normalize).join("\n"));
TypeScriptmain.ts · 26 줄
import { readFileSync } from "node:fs";const lines: string[] = readFileSync(0, "utf8").split("\n");const n: number = Number(lines[0]); function normalize(url: string): string {  // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다  const h: number = url.indexOf("#");  const rest: string = h === -1 ? url : url.slice(0, h);  const frag: string = h === -1 ? "" : url.slice(h);  const q: number = rest.indexOf("?");  const path: string = q === -1 ? rest : rest.slice(0, q);  const query: string = q === -1 ? "" : rest.slice(q);  const stack: string[] = [];  for (const seg of path.split("/")) {    if (seg === "" || seg === ".") continue;    if (seg === "..") stack.pop(); // 빈 배열의 pop()은 오류 없이 undefined    else stack.push(seg);  }  let clean: string = "/" + stack.join("/");  // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다  const last: string = path.slice(path.lastIndexOf("/") + 1);  if (stack.length > 0 && (last === "" || last === "." || last === "..")) clean += "/";  return clean + query + frag;} console.log(lines.slice(1, 1 + n).map(normalize).join("\n"));
JavaMain.java · 39 줄
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.List; public class Main {    static String normalize(String url) {        // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다        int h = url.indexOf('#');        String rest = h == -1 ? url : url.substring(0, h);        String frag = h == -1 ? "" : url.substring(h);        int q = rest.indexOf('?');        String path = q == -1 ? rest : rest.substring(0, q);        String query = q == -1 ? "" : rest.substring(q);        List<String> stack = new ArrayList<>();        for (String seg : path.split("/")) {            if (seg.isEmpty() || seg.equals(".")) continue;            if (seg.equals("..")) {                if (!stack.isEmpty()) stack.remove(stack.size() - 1);            } else {                stack.add(seg);            }        }        String clean = "/" + String.join("/", stack);        // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다        String last = path.substring(path.lastIndexOf('/') + 1);        if (!stack.isEmpty() && (last.isEmpty() || last.equals(".") || last.equals(".."))) clean += "/";        return clean + query + frag;    }     public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        int n = Integer.parseInt(in.readLine().trim());        StringBuilder out = new StringBuilder();        for (int i = 0; i < n; i++) out.append(normalize(in.readLine())).append('\n');        System.out.print(out);    }}
C#Main.cs · 37 줄
using System;using System.Collections.Generic; class Program{    static string Normalize(string url)    {        // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다        int h = url.IndexOf('#');        string rest = h == -1 ? url : url.Substring(0, h);        string frag = h == -1 ? "" : url.Substring(h);        int q = rest.IndexOf('?');        string path = q == -1 ? rest : rest.Substring(0, q);        string query = q == -1 ? "" : rest.Substring(q);        var stack = new List<string>();        foreach (string seg in path.Split('/', StringSplitOptions.RemoveEmptyEntries))        {            if (seg == ".") continue;            if (seg == "..")            {                if (stack.Count > 0) stack.RemoveAt(stack.Count - 1);            }            else stack.Add(seg);        }        string clean = "/" + string.Join("/", stack);        // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다        string last = path.Substring(path.LastIndexOf('/') + 1);        if (stack.Count > 0 && (last == "" || last == "." || last == "..")) clean += "/";        return clean + query + frag;    }     static void Main()    {        int n = int.Parse(Console.ReadLine());        for (int i = 0; i < n; i++) Console.WriteLine(Normalize(Console.ReadLine()));    }}
C++main.cpp · 41 줄
#include <iostream>#include <sstream>#include <string>#include <vector> std::string normalize(const std::string& url) {    // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다    size_t h = url.find('#');    std::string rest = url.substr(0, h);  // h가 npos면 끝까지    std::string frag = h == std::string::npos ? "" : url.substr(h);    size_t q = rest.find('?');    std::string path = rest.substr(0, q);    std::string query = q == std::string::npos ? "" : rest.substr(q);    std::vector<std::string> stack;    std::stringstream ss(path);    std::string seg;    while (std::getline(ss, seg, '/')) {        if (seg.empty() || seg == ".") continue;        if (seg == "..") {            if (!stack.empty()) stack.pop_back();  // 빈 vector의 pop_back은 정의되지 않은 동작        } else {            stack.push_back(seg);        }    }    std::string out = "/";    for (size_t k = 0; k < stack.size(); k++) {        if (k > 0) out += '/';        out += stack[k];    }    // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다    std::string last = path.substr(path.rfind('/') + 1);    if (!stack.empty() && (last.empty() || last == "." || last == "..")) out += '/';    return out + query + frag;} int main() {    std::string line;    std::getline(std::cin, line);    int n = std::stoi(line);    for (int i = 0; i < n && std::getline(std::cin, line); i++) std::cout << normalize(line) << '\n';}
Cmain.c · 42 줄
#include <stdio.h>#include <string.h> int main(void) {    int n;    char url[256];    if (!fgets(url, sizeof url, stdin)) return 0;    sscanf(url, "%d", &n);    for (int i = 0; i < n; i++) {        if (!fgets(url, sizeof url, stdin)) break;        url[strcspn(url, "\n")] = '\0';        /* 조각(#)을 먼저 찾아야 한다: 조각 안의 ?는 질의가 아니다 */        char *h = strchr(url, '#');        char frag[256] = "";        if (h) { strcpy(frag, h); *h = '\0'; }        char *q = strchr(url, '?');        char query[256] = "";        if (q) { strcpy(query, q); *q = '\0'; }        /* 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리킨다(strtok이 url을 바꾸기 전에 본다) */        const char *last = strrchr(url, '/') + 1;        int dirLike = strcmp(last, "") == 0 || strcmp(last, ".") == 0 || strcmp(last, "..") == 0;        /* 이제 url에는 경로만 남았다. strtok은 빈 조각("//")을 저절로 건너뛴다 */        char *stack[128];        int top = 0;        for (char *seg = strtok(url, "/"); seg; seg = strtok(NULL, "/")) {            if (strcmp(seg, ".") == 0) continue;            if (strcmp(seg, "..") == 0) {                if (top > 0) top--;            } else {                stack[top++] = seg;            }        }        char out[512] = "/";        for (int k = 0; k < top; k++) {            if (k > 0) strcat(out, "/");            strcat(out, stack[k]);        }        if (top > 0 && dirLike) strcat(out, "/");        printf("%s%s%s\n", out, query, frag);    }    return 0;}
Craykonmain.cray · 54 줄
n = 콘솔.줄읽기().수로변환();반복 (i = 0; i < n; i++){    콘솔.쓰기(normalize(콘솔.줄읽기()));} normalize(url){    // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다    rest = url;    frag = "";    h = url.자리찾기("#");    만약 (h != -1)    {        rest = url.부분문자열(0, h);        frag = url.부분문자열(h);    }    path = rest;    query = "";    q = rest.자리찾기("?");    만약 (q != -1)    {        path = rest.부분문자열(0, q);        query = rest.부분문자열(q);    }    segs = path.나누기("/");    stack = [];    각각 (seg 에서 segs)    {        만약 (seg == "" || seg == ".")        {            건너뛰기;        }        만약 (seg == "..")        {            만약 (stack.개수 > 0)            {                stack.자리빼기(stack.개수 - 1);            }        }        아니면        {            stack.넣기(seg);        }    }    clean = "/" + stack.잇기("/");    // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다    last = segs[segs.개수 - 1];    만약 (stack.개수 > 0 && (last == "" || last == "." || last == ".."))    {        clean += "/";    }    돌려주기 clean + query + frag;}